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().min(20, "MFA transaction is invalid").max(512).optional(), 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; 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; export function authBrowserSchemaDescriptors( schemas: AuthSchemaSet = authSchemas, ): Record { const descriptors: Record = {}; for (const [browserName, schemaName] of Object.entries(authBrowserSchemaMap)) { descriptors[browserName] = schemas[schemaName].describe(); } return descriptors; }