release: WRNexusJS 0.5.0
This commit is contained in:
@@ -0,0 +1,355 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user