better-auth-harmony
Version:
Validation and normalization for better-auth
382 lines (379 loc) • 21.4 kB
TypeScript
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 };