better-auth
Version:
The most comprehensive authentication library for TypeScript.
798 lines (792 loc) • 25.5 kB
JavaScript
import { APIError, toResponse, createRouter } from 'better-call';
export { APIError } from 'better-call';
import { a as createAuthEndpoint, B as BASE_ERROR_CODES, e as createEmailVerificationToken, w as wildcardMatch, l as listSessions, u as updateUser, b as getSession, i as originCheckMiddleware, j as error, k as ok, m as accountInfo, n as getAccessToken, r as refreshToken, p as unlinkAccount, q as deleteUserCallback, t as listUserAccounts, v as linkSocialAccount, x as revokeOtherSessions, y as revokeSessions, z as revokeSession, A as requestPasswordResetCallback, C as requestPasswordReset, D as forgetPasswordCallback, E as deleteUser, F as setPassword, G as changePassword, I as changeEmail, J as sendVerificationEmail, K as verifyEmail, L as resetPassword, M as forgetPassword, N as signInEmail, O as signOut, P as callbackOAuth, Q as signInSocial } from '../shared/better-auth.oUCDPPbQ.mjs';
export { c as createAuthMiddleware, f as freshSessionMiddleware, g as getSessionFromCtx, S as optionsMiddleware, o as originCheck, R as requestOnlySessionMiddleware, d as sendVerificationEmailFn, s as sessionMiddleware } from '../shared/better-auth.oUCDPPbQ.mjs';
import { z } from 'zod';
import { setSessionCookie } from '../cookies/index.mjs';
import { f as parseUserInput } from '../shared/better-auth.Cc72UxUH.mjs';
import { b as isDevelopment } from '../shared/better-auth.8zoxzg-F.mjs';
import { l as logger } from '../shared/better-auth.Cqykj82J.mjs';
import { g as getIp } from '../shared/better-auth.iKoUsdFE.mjs';
import defu from 'defu';
import '@better-auth/utils/random';
import '../shared/better-auth.dn8_oqOu.mjs';
import '@better-auth/utils/hash';
import '@noble/ciphers/chacha';
import '@noble/ciphers/utils';
import '@noble/ciphers/webcrypto';
import '@better-auth/utils/base64';
import 'jose';
import '@noble/hashes/scrypt';
import '@better-auth/utils';
import '@better-auth/utils/hex';
import '@noble/hashes/utils';
import '../shared/better-auth.B4Qoxdgc.mjs';
import '../social-providers/index.mjs';
import '@better-fetch/fetch';
import '../shared/better-auth.DufyW0qf.mjs';
import '../shared/better-auth.CW6D9eSx.mjs';
import '../shared/better-auth.DdzSJf-n.mjs';
import '../shared/better-auth.tB5eU6EY.mjs';
import '../shared/better-auth.BUPPRXfK.mjs';
import '@better-auth/utils/hmac';
import '@better-auth/utils/binary';
import '../shared/better-auth.DDEbWX-S.mjs';
import '../shared/better-auth.VTXNLFMT.mjs';
import 'jose/errors';
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"
},
callbackURL: {
type: "string",
description: "The URL to use for email verification callback"
}
},
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
}
}
}
}
}
}
}
},
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, ...additionalFields } = body;
const isValidEmail = z.string().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
);
if (!session) {
throw new APIError("BAD_REQUEST", {
message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION
});
}
await setSessionCookie(ctx, {
session,
user: createdUser
});
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, modelName) {
const model = ctx.options.rateLimit?.modelName || "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: "rateLimit",
where: [{ field: "key", value: key }],
update: {
count: value.count,
lastRequest: value.lastRequest
}
});
} else {
await db.create({
model: "rateLimit",
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 stringified = await ctx.options.secondaryStorage?.get(key);
return stringified ? JSON.parse(stringified) : 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, ctx.rateLimit.modelName);
}
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;
}
}
}
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 toAuthEndpoints(endpoints, ctx) {
const api = {};
for (const [key, endpoint] of Object.entries(endpoints)) {
api[key] = async (context) => {
const authContext = await ctx;
let internalContext = {
...context,
context: {
...authContext,
returned: void 0,
responseHeaders: void 0,
session: null
},
path: endpoint.path,
headers: context?.headers ? new Headers(context?.headers) : void 0
};
const { beforeHooks, afterHooks } = getHooks(authContext);
const before = await runBeforeHooks(internalContext, beforeHooks);
if ("context" in before && before.context && typeof before.context === "object") {
const { headers, ...rest } = before.context;
if (headers) {
headers.forEach((value, key2) => {
internalContext.headers.set(key2, value);
});
}
internalContext = defu(rest, internalContext);
} else if (before) {
return before;
}
internalContext.asResponse = false;
internalContext.returnHeaders = true;
const result = await endpoint(internalContext).catch((e) => {
if (e instanceof APIError) {
return {
response: e,
headers: e.headers ? new Headers(e.headers) : null
};
}
throw e;
});
internalContext.context.returned = result.response;
internalContext.context.responseHeaders = result.headers;
const after = await runAfterHooks(internalContext, afterHooks);
if (after.response) {
result.response = after.response;
}
if (result.response instanceof APIError && !context?.asResponse) {
throw result.response;
}
const response = context?.asResponse ? toResponse(result.response, {
headers: result.headers
}) : context?.returnHeaders ? {
headers: result.headers,
response: result.response
} : result.response;
return response;
};
api[key].path = endpoint.path;
api[key].options = endpoint.options;
}
return api;
}
async function runBeforeHooks(context, hooks) {
let modifiedContext = {};
for (const hook of hooks) {
if (hook.matcher(context)) {
const result = await hook.handler({
...context,
returnHeaders: false
});
if (result && typeof result === "object") {
if ("context" in result && typeof result.context === "object") {
const { headers, ...rest } = result.context;
if (headers instanceof Headers) {
if (modifiedContext.headers) {
headers.forEach((value, key) => {
modifiedContext.headers?.set(key, value);
});
} else {
modifiedContext.headers = headers;
}
}
modifiedContext = defu(rest, modifiedContext);
continue;
}
return result;
}
}
}
return { context: modifiedContext };
}
async function runAfterHooks(context, hooks) {
for (const hook of hooks) {
if (hook.matcher(context)) {
const result = await hook.handler(context).catch((e) => {
if (e instanceof APIError) {
return {
response: e,
headers: e.headers ? new Headers(e.headers) : null
};
}
throw e;
});
if (result.headers) {
result.headers.forEach((value, key) => {
if (!context.context.responseHeaders) {
context.context.responseHeaders = new Headers({
[key]: value
});
} else {
if (key.toLowerCase() === "set-cookie") {
context.context.responseHeaders.append(key, value);
} else {
context.context.responseHeaders.set(key, value);
}
}
});
}
if (result.response) {
context.context.returned = result.response;
}
}
}
return {
response: context.context.returned,
headers: context.context.responseHeaders
};
}
function getHooks(authContext) {
const plugins = authContext.options.plugins || [];
const beforeHooks = [];
const afterHooks = [];
if (authContext.options.hooks?.before) {
beforeHooks.push({
matcher: () => true,
handler: authContext.options.hooks.before
});
}
if (authContext.options.hooks?.after) {
afterHooks.push({
matcher: () => true,
handler: authContext.options.hooks.after
});
}
const pluginBeforeHooks = plugins.map((plugin) => {
if (plugin.hooks?.before) {
return plugin.hooks.before;
}
}).filter((plugin) => plugin !== void 0).flat();
const pluginAfterHooks = plugins.map((plugin) => {
if (plugin.hooks?.after) {
return plugin.hooks.after;
}
}).filter((plugin) => plugin !== void 0).flat();
pluginBeforeHooks.length && beforeHooks.push(...pluginBeforeHooks);
pluginAfterHooks.length && afterHooks.push(...pluginAfterHooks);
return {
beforeHooks,
afterHooks
};
}
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) => {
return m.middleware({
...context,
context: {
...ctx,
...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, 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 };