356 lines
12 KiB
TypeScript
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;
|
|
}
|