Files
WRNexusJS/packages/auth/src/validation.ts
T
2026-07-29 12:51:10 +05:30

356 lines
12 KiB
TypeScript

import { v, type ObjectSchema, type SchemaDescriptor } from "@wrnexus/validation";
const strongPassword = () =>
v
.string()
.required("Enter your password")
.min(12, "Password must be at least 12 characters")
.max(256, "Password must be at most 256 characters")
.pattern(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/,
"Password must include uppercase, lowercase, and a number",
);
/** Generic server-side registration input for custom registration experiences. */
export const registerSchema = v.object({
displayName: v.string().trim().min(2, "Enter your full name").max(120),
email: v.string().trim().email("Enter a valid email address").optional(),
phone: v.string().trim().min(7, "Enter a valid phone number").max(24).optional(),
username: v
.string()
.trim()
.min(3, "Username must be at least 3 characters")
.max(64)
.pattern(/^[a-zA-Z0-9._-]+$/, "Use only letters, numbers, dots, underscores, or hyphens")
.optional(),
password: strongPassword(),
locale: v.string().max(32).optional(),
timezone: v.string().max(64).optional(),
});
/** Default schema shared by the packaged SignUp component and register route. */
export const signUpSchema = registerSchema.extend({
email: v
.string()
.trim()
.required("Enter your email address")
.email("Enter a valid email address"),
consent: v.boolean().required("Accept the terms and privacy policy to continue"),
});
export const loginSchema = v.object({
identifier: v.string().trim().required("Enter your email, phone, or username").max(320),
password: v.string().required("Enter your password").max(256),
returnTo: v.string().max(2048).optional(),
rememberDevice: v.boolean().optional(),
deviceFingerprint: v.string().max(512).optional(),
deviceName: v.string().max(120).optional(),
});
export const verificationRequestSchema = v.object({
type: v
.string()
.required("Choose email or phone verification")
.oneOf(["email", "phone"], "Choose email or phone verification"),
identifier: v.string().trim().max(320).optional(),
});
export const verificationTokenSchema = v.object({
token: v
.string()
.trim()
.required("Enter the verification token")
.min(6, "Verification token is too short")
.max(512),
});
export const passwordResetRequestSchema = v.object({
identifier: v
.string()
.trim()
.required("Enter your email, phone, or username")
.max(320, "Account identifier is too long"),
});
export const passwordResetSchema = v.object({
token: v
.string()
.required("Password reset token is missing")
.min(20, "Password reset token is invalid")
.max(512),
password: strongPassword(),
});
export const invitationAcceptSchema = v.object({
token: v
.string()
.required("Invitation token is missing")
.min(20, "Invitation token is invalid")
.max(512),
displayName: v.string().trim().min(2, "Enter your full name").max(120).optional(),
password: strongPassword().optional(),
});
export const magicLinkRequestSchema = v.object({
identifier: v.string().trim().required("Enter your email, phone, or username").max(320),
});
export const magicLinkConsumeSchema = v.object({
token: v
.string()
.required("Magic-link token is missing")
.min(20, "Magic-link token is invalid")
.max(512),
returnTo: v.string().max(2048).optional(),
});
export const otpLoginRequestSchema = v.object({
identifier: v.string().trim().required("Enter your email, phone, or username").max(320),
method: v
.string()
.required("Choose an OTP delivery method")
.oneOf(["email-otp", "sms-otp"], "Choose email or SMS OTP"),
});
export const otpLoginCompleteSchema = v.object({
challengeId: v
.string()
.required("OTP challenge is missing")
.min(8, "OTP challenge is invalid")
.max(191),
code: v
.string()
.trim()
.required("Enter the one-time code")
.pattern(/^\d{6}$/, "Enter the six-digit one-time code"),
returnTo: v.string().max(2048).optional(),
});
export const otpIssueSchema = v.object({
method: v
.string()
.required("Choose an OTP delivery method")
.oneOf(["email-otp", "sms-otp"], "Choose email or SMS OTP"),
destination: v.string().trim().max(320).optional(),
});
export const otpSchema = v.object({
challengeId: v
.string()
.required("OTP challenge is missing")
.min(8, "OTP challenge is invalid")
.max(191),
code: v
.string()
.trim()
.required("Enter the one-time code")
.pattern(/^\d{6}$/, "Enter the six-digit one-time code"),
});
export const mfaOtpRequestSchema = v.object({
mfaToken: v
.string()
.required("MFA transaction is missing")
.min(20, "MFA transaction is invalid")
.max(512),
method: v
.string()
.required("Choose an MFA delivery method")
.oneOf(["email-otp", "sms-otp"], "Choose email or SMS OTP"),
});
export const mfaSchema = v.object({
mfaToken: v
.string()
.required("MFA transaction is missing")
.min(20, "MFA transaction is invalid")
.max(512),
method: v
.string()
.required("Choose a verification method")
.oneOf(
["totp", "recovery-code", "email-otp", "sms-otp"],
"Choose a supported verification method",
),
challengeId: v.string().max(191).optional(),
code: v
.string()
.trim()
.required("Enter the verification code")
.min(6, "Verification code is too short")
.max(32),
returnTo: v.string().max(2048).optional(),
});
export const sessionRevokeSchema = v.object({
sessionId: v.string().required("Session ID is missing").min(3).max(191),
});
export const impersonationStartSchema = v.object({
targetUserId: v.string().required("Choose a user to impersonate").min(3).max(191),
reason: v.string().trim().max(500).optional(),
});
export const passkeyRegistrationOptionsSchema = v.object({
rpId: v.string().trim().max(253).optional(),
rpName: v.string().trim().max(120).optional(),
origin: v.string().trim().url("Enter a valid origin").optional(),
});
export const passkeyRegistrationVerifySchema = v.object({
key: v.string().required("Passkey challenge key is missing").min(8).max(512),
response: v.unknown().required("Passkey response is missing"),
rpId: v.string().trim().max(253).optional(),
origin: v.string().trim().url("Enter a valid origin").optional(),
name: v.string().trim().max(120).optional(),
});
export const passkeyAuthenticationOptionsSchema = v.object({
identifier: v.string().trim().max(320).optional(),
rpId: v.string().trim().max(253).optional(),
origin: v.string().trim().url("Enter a valid origin").optional(),
});
export const passkeyAuthenticationVerifySchema = v.object({
key: v.string().required("Passkey challenge key is missing").min(8).max(512),
response: v.unknown().required("Passkey response is missing"),
rpId: v.string().trim().max(253).optional(),
origin: v.string().trim().url("Enter a valid origin").optional(),
});
export const authenticatorSetupSchema = v.object({
label: v.string().trim().max(120).optional(),
});
export const authenticatorConfirmSchema = v.object({
credentialId: v.string().required("Authenticator credential is missing").min(3).max(191),
code: v
.string()
.trim()
.required("Enter the authenticator code")
.pattern(/^\d{6}$/, "Enter the six-digit authenticator code"),
});
export const authenticatorDisableSchema = v.object({
credentialId: v.string().required("Authenticator credential is missing").min(3).max(191),
});
export const recoveryCodesSchema = v.object({
count: v.number().integer("Recovery code count must be a whole number").min(1).max(50).optional(),
});
export const emptyActionSchema = v.object({});
export const changePasswordSchema = v.object({
currentPassword: v.string().required("Enter your current password").max(256),
nextPassword: strongPassword(),
});
export interface AuthSchemaSet {
register: ObjectSchema;
signUp: ObjectSchema;
login: ObjectSchema;
verificationRequest: ObjectSchema;
verificationToken: ObjectSchema;
passwordResetRequest: ObjectSchema;
passwordReset: ObjectSchema;
invitationAccept: ObjectSchema;
magicLinkRequest: ObjectSchema;
magicLinkConsume: ObjectSchema;
otpLoginRequest: ObjectSchema;
otpLoginComplete: ObjectSchema;
otpIssue: ObjectSchema;
otpVerify: ObjectSchema;
mfaOtpRequest: ObjectSchema;
mfaComplete: ObjectSchema;
sessionRevoke: ObjectSchema;
impersonationStart: ObjectSchema;
passkeyRegistrationOptions: ObjectSchema;
passkeyRegistrationVerify: ObjectSchema;
passkeyAuthenticationOptions: ObjectSchema;
passkeyAuthenticationVerify: ObjectSchema;
authenticatorSetup: ObjectSchema;
authenticatorConfirm: ObjectSchema;
authenticatorDisable: ObjectSchema;
recoveryCodes: ObjectSchema;
emptyAction: ObjectSchema;
changePassword: ObjectSchema;
}
export type AuthSchemaOverrides = Partial<AuthSchemaSet>;
export const authSchemas: AuthSchemaSet = {
register: signUpSchema,
signUp: signUpSchema,
login: loginSchema,
verificationRequest: verificationRequestSchema,
verificationToken: verificationTokenSchema,
passwordResetRequest: passwordResetRequestSchema,
passwordReset: passwordResetSchema,
invitationAccept: invitationAcceptSchema,
magicLinkRequest: magicLinkRequestSchema,
magicLinkConsume: magicLinkConsumeSchema,
otpLoginRequest: otpLoginRequestSchema,
otpLoginComplete: otpLoginCompleteSchema,
otpIssue: otpIssueSchema,
otpVerify: otpSchema,
mfaOtpRequest: mfaOtpRequestSchema,
mfaComplete: mfaSchema,
sessionRevoke: sessionRevokeSchema,
impersonationStart: impersonationStartSchema,
passkeyRegistrationOptions: passkeyRegistrationOptionsSchema,
passkeyRegistrationVerify: passkeyRegistrationVerifySchema,
passkeyAuthenticationOptions: passkeyAuthenticationOptionsSchema,
passkeyAuthenticationVerify: passkeyAuthenticationVerifySchema,
authenticatorSetup: authenticatorSetupSchema,
authenticatorConfirm: authenticatorConfirmSchema,
authenticatorDisable: authenticatorDisableSchema,
recoveryCodes: recoveryCodesSchema,
emptyAction: emptyActionSchema,
changePassword: changePasswordSchema,
};
export function resolveAuthSchemas(overrides: AuthSchemaOverrides = {}): AuthSchemaSet {
return { ...authSchemas, ...overrides };
}
export const authBrowserSchemaMap = {
"auth-register": "register",
"auth-login": "login",
"auth-verification-request": "verificationRequest",
"auth-verification-token": "verificationToken",
"auth-password-request": "passwordResetRequest",
"auth-password-reset": "passwordReset",
"auth-invitation": "invitationAccept",
"auth-magic-link-request": "magicLinkRequest",
"auth-magic-link-consume": "magicLinkConsume",
"auth-otp-login-request": "otpLoginRequest",
"auth-otp-login-complete": "otpLoginComplete",
"auth-otp-issue": "otpIssue",
"auth-otp": "otpVerify",
"auth-mfa-otp-request": "mfaOtpRequest",
"auth-mfa": "mfaComplete",
"auth-session-revoke": "sessionRevoke",
"auth-impersonation-start": "impersonationStart",
"auth-passkey-registration-options": "passkeyRegistrationOptions",
"auth-passkey-registration-verify": "passkeyRegistrationVerify",
"auth-passkey-authentication-options": "passkeyAuthenticationOptions",
"auth-passkey-authentication-verify": "passkeyAuthenticationVerify",
"auth-authenticator-setup": "authenticatorSetup",
"auth-authenticator-confirm": "authenticatorConfirm",
"auth-authenticator-disable": "authenticatorDisable",
"auth-recovery-codes": "recoveryCodes",
"auth-empty": "emptyAction",
"auth-change-password": "changePassword",
} as const satisfies Record<string, keyof AuthSchemaSet>;
export function authBrowserSchemaDescriptors(
schemas: AuthSchemaSet = authSchemas,
): Record<string, SchemaDescriptor> {
const descriptors: Record<string, SchemaDescriptor> = {};
for (const [browserName, schemaName] of Object.entries(authBrowserSchemaMap)) {
descriptors[browserName] = schemas[schemaName].describe();
}
return descriptors;
}