UNPKG

better-auth

Version:

The most comprehensive authentication library for TypeScript.

717 lines (711 loc) 22.7 kB
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 };