UNPKG

better-auth-harmony

Version:

Validation and normalization for better-auth

382 lines (379 loc) 21.4 kB
import * as better_call from 'better-call'; import * as better_auth from 'better-auth'; import { User } from 'better-auth'; import { APIError } from 'better-auth/api'; import { BetterAuthPlugin } from 'better-auth/types'; import normalizeEmail from 'validator/lib/normalizeEmail.js'; import { Matcher } from './email/matchers.js'; interface UserWithNormalizedEmail extends User { normalizedEmail?: string | null; } interface EmailHarmonySchema { user: { fields: { /** Map the normalizedEmail field name to a custom value */ normalizedEmail?: string; }; }; } interface EmailHarmonyOptions { /** * Allow logging in with any version of the unnormalized email address. Also works for password * reset. For example a user who signed up with the email `johndoe@googlemail.com` may also log * in with `john.doe@gmail.com`. Makes 1 extra database query for every login attempt. * @default false */ allowNormalizedSignin?: boolean; /** * Function to validate email. Default is `validateEmail`. */ validator?: (email: string) => boolean | Promise<boolean>; /** * Function to normalize email address. Default is `validator.normalizeEmail(t)`. * @see https://github.com/validatorjs/validator.js#sanitizers */ normalizer?: typeof normalizeEmail; /** * Specify in which routes the plugin should run, for example by path. * @example <caption>Ready-made matchers</caption> * import * as matchers from 'better-auth-harmony/email/matchers'; * * export const auth = betterAuth({ * // ... other config options * plugins: [ * emailHarmony({ * matchers: { * signIn: [matchers.emailOtpVerify, matchers.emailOtpForget, matchers.emailOtpReset] * } * }) * ] * }); */ matchers?: { /** * Specify where the plugin should run to look up users by normalized email address if * `allowNormalizedSignin` is true. * @default [`allEmailSignIn`] */ signIn?: Matcher[]; /** * Specify which requests the plugin should validate, for example in which routes by path. * @default [`allEmail`] */ validation?: Matcher[]; }; /** Pass the `schema` option to customize the field names */ schema?: EmailHarmonySchema; } /** * The default validation function runs `validator.isEmail(email) && Mailchecker.isValid(email)`. * @see https://github.com/validatorjs/validator.js#validators * @see https://github.com/FGRibreau/mailchecker * @param email The email address to validate * @returns Boolean indicating whether the address should be accepted (`true`), or rejected * (`false`). */ declare const validateEmail: (email: string) => boolean; declare const emailHarmony: ({ allowNormalizedSignin, validator, matchers, schema, normalizer }?: EmailHarmonyOptions) => { id: "harmony-email"; init(): { options: { databaseHooks: { user: { create: { before: (user: Partial<User> & { phoneNumber?: string; }) => Promise<false | { data: Required<User>; } | { data: { normalizedEmail: string; id: string; createdAt: Date; updatedAt: Date; email: string; emailVerified: boolean; name: string; image: string | null; }; }>; }; update: { before: (user: Partial<User> & { phoneNumber?: string; }) => Promise<false | { data: Required<User>; } | { data: { normalizedEmail: string; id: string; createdAt: Date; updatedAt: Date; email: string; emailVerified: boolean; name: string; image: string | null; }; }>; }; }; }; }; }; schema: { user: { fields: { normalizedEmail: { type: "string"; fieldName: string; required: false; unique: true; input: false; returned: false; }; }; }; }; hooks: { before: ({ matcher: (context: better_auth.HookEndpointContext) => boolean; handler: (inputContext: better_call.MiddlewareInputContext<better_call.MiddlewareOptions>) => Promise<void>; } | { matcher: (context: better_auth.HookEndpointContext) => boolean; handler: (inputContext: better_call.MiddlewareInputContext<better_call.MiddlewareOptions>) => Promise<{ context: { query: { email: string; normalizedEmail: string; }; method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; path: string; body: any; params: Record<string, any> & string; request: Request | undefined; headers: Headers | undefined; setHeader: ((key: string, value: string) => void) & ((key: string, value: string) => void); setStatus: (status: better_call.Status) => void; getHeader: ((key: string) => string | null) & ((key: string) => string | null); getCookie: (key: string, prefix?: better_call.CookiePrefixOptions) => string | null; getSignedCookie: (key: string, secret: string, prefix?: better_call.CookiePrefixOptions) => Promise<string | null | false>; setCookie: (key: string, value: string, options?: better_call.CookieOptions) => string; setSignedCookie: (key: string, value: string, secret: string, options?: better_call.CookieOptions) => Promise<string>; json: (<R extends Record<string, any> | null>(json: R, routerResponse?: { status?: number; headers?: Record<string, string>; response?: Response; body?: Record<string, string>; } | Response) => Promise<R>) & (<R extends Record<string, any> | null>(json: R, routerResponse?: { status?: number; headers?: Record<string, string>; response?: Response; } | Response) => Promise<R>); context: { [x: string]: any; } & { returned?: unknown | undefined; responseHeaders?: Headers | undefined; getPlugin: <Plugin extends BetterAuthPlugin>(pluginId: Plugin["id"]) => Plugin | null; appName: string; baseURL: string; version: string; options: better_auth.BetterAuthOptions; trustedOrigins: string[]; isTrustedOrigin: (url: string, settings?: { allowRelativePaths: boolean; }) => boolean; oauthConfig: { skipStateCookieCheck?: boolean | undefined; storeStateStrategy: "database" | "cookie"; }; newSession: { session: better_auth.Session & Record<string, any>; user: User & Record<string, any>; } | null; session: { session: better_auth.Session & Record<string, any>; user: User & Record<string, any>; } | null; setNewSession: (session: { session: better_auth.Session & Record<string, any>; user: User & Record<string, any>; } | null) => void; socialProviders: better_auth.OAuthProvider[]; authCookies: better_auth.BetterAuthCookies; logger: ReturnType<(options?: better_auth.Logger | undefined) => better_auth.InternalLogger>; rateLimit: { enabled: boolean; window: number; max: number; storage: "memory" | "database" | "secondary-storage"; } & Omit<better_auth.BetterAuthRateLimitOptions, "enabled" | "window" | "max" | "storage">; adapter: better_auth.DBAdapter<better_auth.BetterAuthOptions>; internalAdapter: better_auth.InternalAdapter<better_auth.BetterAuthOptions>; createAuthCookie: (cookieName: string, overrideAttributes?: Partial<better_call.CookieOptions> | undefined) => better_auth.BetterAuthCookie; secret: string; sessionConfig: { updateAge: number; expiresIn: number; freshAge: number; cookieRefreshCache: false | { enabled: true; updateAge: number; }; }; generateId: (options: { model: better_auth.ModelNames; size?: number | undefined; }) => string | false; secondaryStorage: better_auth.SecondaryStorage | undefined; password: { hash: (password: string) => Promise<string>; verify: (data: { password: string; hash: string; }) => Promise<boolean>; config: { minPasswordLength: number; maxPasswordLength: number; }; checkPassword: (userId: string, ctx: better_auth.GenericEndpointContext<better_auth.BetterAuthOptions>) => Promise<boolean>; }; tables: better_auth.BetterAuthDBSchema; runMigrations: () => Promise<void>; publishTelemetry: (event: { type: string; anonymousId?: string | undefined; payload: Record<string, any>; }) => Promise<void>; skipOriginCheck: boolean | string[]; skipCSRFCheck: boolean; runInBackground: (promise: Promise<unknown>) => void; runInBackgroundOrAwait: (promise: Promise<unknown> | void) => better_auth.Awaitable<unknown>; }; redirect: (url: string) => APIError; error: (status: ("OK" | "CREATED" | "ACCEPTED" | "NO_CONTENT" | "MULTIPLE_CHOICES" | "MOVED_PERMANENTLY" | "FOUND" | "SEE_OTHER" | "NOT_MODIFIED" | "TEMPORARY_REDIRECT" | "BAD_REQUEST" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_ALLOWED" | "NOT_ACCEPTABLE" | "PROXY_AUTHENTICATION_REQUIRED" | "REQUEST_TIMEOUT" | "CONFLICT" | "GONE" | "LENGTH_REQUIRED" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "URI_TOO_LONG" | "UNSUPPORTED_MEDIA_TYPE" | "RANGE_NOT_SATISFIABLE" | "EXPECTATION_FAILED" | "I'M_A_TEAPOT" | "MISDIRECTED_REQUEST" | "UNPROCESSABLE_ENTITY" | "LOCKED" | "FAILED_DEPENDENCY" | "TOO_EARLY" | "UPGRADE_REQUIRED" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "REQUEST_HEADER_FIELDS_TOO_LARGE" | "UNAVAILABLE_FOR_LEGAL_REASONS" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "HTTP_VERSION_NOT_SUPPORTED" | "VARIANT_ALSO_NEGOTIATES" | "INSUFFICIENT_STORAGE" | "LOOP_DETECTED" | "NOT_EXTENDED" | "NETWORK_AUTHENTICATION_REQUIRED") | better_call.Status, body?: { message?: string; code?: string; } & Record<string, any>, headers?: HeadersInit) => APIError; }; } | { context: { body: { email: string; normalizedEmail: string; }; method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; path: string; query: Record<string, any> | undefined; params: Record<string, any> & string; request: Request | undefined; headers: Headers | undefined; setHeader: ((key: string, value: string) => void) & ((key: string, value: string) => void); setStatus: (status: better_call.Status) => void; getHeader: ((key: string) => string | null) & ((key: string) => string | null); getCookie: (key: string, prefix?: better_call.CookiePrefixOptions) => string | null; getSignedCookie: (key: string, secret: string, prefix?: better_call.CookiePrefixOptions) => Promise<string | null | false>; setCookie: (key: string, value: string, options?: better_call.CookieOptions) => string; setSignedCookie: (key: string, value: string, secret: string, options?: better_call.CookieOptions) => Promise<string>; json: (<R extends Record<string, any> | null>(json: R, routerResponse?: { status?: number; headers?: Record<string, string>; response?: Response; body?: Record<string, string>; } | Response) => Promise<R>) & (<R extends Record<string, any> | null>(json: R, routerResponse?: { status?: number; headers?: Record<string, string>; response?: Response; } | Response) => Promise<R>); context: { [x: string]: any; } & { returned?: unknown | undefined; responseHeaders?: Headers | undefined; getPlugin: <Plugin extends BetterAuthPlugin>(pluginId: Plugin["id"]) => Plugin | null; appName: string; baseURL: string; version: string; options: better_auth.BetterAuthOptions; trustedOrigins: string[]; isTrustedOrigin: (url: string, settings?: { allowRelativePaths: boolean; }) => boolean; oauthConfig: { skipStateCookieCheck?: boolean | undefined; storeStateStrategy: "database" | "cookie"; }; newSession: { session: better_auth.Session & Record<string, any>; user: User & Record<string, any>; } | null; session: { session: better_auth.Session & Record<string, any>; user: User & Record<string, any>; } | null; setNewSession: (session: { session: better_auth.Session & Record<string, any>; user: User & Record<string, any>; } | null) => void; socialProviders: better_auth.OAuthProvider[]; authCookies: better_auth.BetterAuthCookies; logger: ReturnType<(options?: better_auth.Logger | undefined) => better_auth.InternalLogger>; rateLimit: { enabled: boolean; window: number; max: number; storage: "memory" | "database" | "secondary-storage"; } & Omit<better_auth.BetterAuthRateLimitOptions, "enabled" | "window" | "max" | "storage">; adapter: better_auth.DBAdapter<better_auth.BetterAuthOptions>; internalAdapter: better_auth.InternalAdapter<better_auth.BetterAuthOptions>; createAuthCookie: (cookieName: string, overrideAttributes?: Partial<better_call.CookieOptions> | undefined) => better_auth.BetterAuthCookie; secret: string; sessionConfig: { updateAge: number; expiresIn: number; freshAge: number; cookieRefreshCache: false | { enabled: true; updateAge: number; }; }; generateId: (options: { model: better_auth.ModelNames; size?: number | undefined; }) => string | false; secondaryStorage: better_auth.SecondaryStorage | undefined; password: { hash: (password: string) => Promise<string>; verify: (data: { password: string; hash: string; }) => Promise<boolean>; config: { minPasswordLength: number; maxPasswordLength: number; }; checkPassword: (userId: string, ctx: better_auth.GenericEndpointContext<better_auth.BetterAuthOptions>) => Promise<boolean>; }; tables: better_auth.BetterAuthDBSchema; runMigrations: () => Promise<void>; publishTelemetry: (event: { type: string; anonymousId?: string | undefined; payload: Record<string, any>; }) => Promise<void>; skipOriginCheck: boolean | string[]; skipCSRFCheck: boolean; runInBackground: (promise: Promise<unknown>) => void; runInBackgroundOrAwait: (promise: Promise<unknown> | void) => better_auth.Awaitable<unknown>; }; redirect: (url: string) => APIError; error: (status: ("OK" | "CREATED" | "ACCEPTED" | "NO_CONTENT" | "MULTIPLE_CHOICES" | "MOVED_PERMANENTLY" | "FOUND" | "SEE_OTHER" | "NOT_MODIFIED" | "TEMPORARY_REDIRECT" | "BAD_REQUEST" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_ALLOWED" | "NOT_ACCEPTABLE" | "PROXY_AUTHENTICATION_REQUIRED" | "REQUEST_TIMEOUT" | "CONFLICT" | "GONE" | "LENGTH_REQUIRED" | "PRECONDITION_FAILED" | "PAYLOAD_TOO_LARGE" | "URI_TOO_LONG" | "UNSUPPORTED_MEDIA_TYPE" | "RANGE_NOT_SATISFIABLE" | "EXPECTATION_FAILED" | "I'M_A_TEAPOT" | "MISDIRECTED_REQUEST" | "UNPROCESSABLE_ENTITY" | "LOCKED" | "FAILED_DEPENDENCY" | "TOO_EARLY" | "UPGRADE_REQUIRED" | "PRECONDITION_REQUIRED" | "TOO_MANY_REQUESTS" | "REQUEST_HEADER_FIELDS_TOO_LARGE" | "UNAVAILABLE_FOR_LEGAL_REASONS" | "INTERNAL_SERVER_ERROR" | "NOT_IMPLEMENTED" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT" | "HTTP_VERSION_NOT_SUPPORTED" | "VARIANT_ALSO_NEGOTIATES" | "INSUFFICIENT_STORAGE" | "LOOP_DETECTED" | "NOT_EXTENDED" | "NETWORK_AUTHENTICATION_REQUIRED") | better_call.Status, body?: { message?: string; code?: string; } & Record<string, any>, headers?: HeadersInit) => APIError; }; } | undefined>; })[]; }; }; export { type EmailHarmonyOptions, type EmailHarmonySchema, type UserWithNormalizedEmail, emailHarmony as default, validateEmail };