Cadastro e verificação
Por que o cadastro sempre devolve um desafio, e a única ramificação que atende todos os modos.
O signUp nunca autentica ninguém sozinho. Ele devolve um desafio — o
endereço ou telefone precisa ser confirmado antes — e a cara desse desafio
depende de como o ambiente está configurado.
O ponto desta página é que você escreve a ramificação uma vez e nunca mais mexe nela quando essa configuração mudar.
const { data, error } = await nulvless.auth.signUp({ email, password });
if (error) return;
if (data.challenge?.preferred === 'link') {
// Nada a coletar — um link foi enviado por e-mail.
}
if (data.challenge) {
// Um código foi enviado. Colete.
}O que decide qual é
Uma configuração do ambiente, Inscrições → Tipo de confirmação:
otpUm código é enviado. A conta é criada quando o código é verificado.linkUm link é enviado por e-mail. Exige um endereço de e-mail.Uma segunda configuração, Autenticar após inscrição, decide se a conta — uma vez confirmada — também produz uma sessão.
confirmationType: 'otp'
O signUp devolve um desafio com subject: 'sign_up' e preferred como
email_otp quando o usuário tem e-mail, sms_otp caso contrário.
const { data } = await nulvless.auth.signUp({ email, password });
if (data.challenge) {
// Colete o código, e então:
const { data: done } = await nulvless.auth.verifyChallenge({
token: data.challenge.token,
code,
});
}A conta ainda não existe
Neste caminho o usuário não é criado pelo signUp. Os dados enviados ficam
carregados no desafio e a conta é criada quando o código é verificado.
Duas consequências que valem ser consideradas no design: um cadastro abandonado não deixa nada para trás, e o e-mail não fica reservado até a confirmação — então duas pessoas podem começar um cadastro com o mesmo endereço, e a segunda a confirmar vai falhar.
Depois de um verifyChallenge bem-sucedido, o authenticateAfterRegistering
vale de novo: você recebe uma sessão, ou só o usuário.
confirmationType: 'link'
O desafio volta com preferred: 'link', o que significa que não há nada a
coletar. Mostre uma tela de "confira seu e-mail" em vez de um campo de código:
if (data.challenge?.preferred === 'link') {
setCheckYourEmail(true);
return;
}O link cai na URL configurada como confirmAccountLinkUri, carregando um token.
A sua página finaliza:
const token = new URLSearchParams(location.search).get('token');
await nulvless.auth.activate(token);O activate não devolve sessão. Confirmar por link prova o endereço; não
autentica a pessoa. Mande ela para a tela de entrada depois.
Este caminho exige e-mail — um ambiente em link com cadastro só por telefone é
rejeitado.
Um componente para todo código
Como o desafio carrega o subject, a mesma tela serve cadastro, entrada por OTP,
MFA e sudo:
function VerifyCode({ challenge }: { challenge: Challenge }) {
return (
<form onSubmit={submit}>
<p>Enviamos um código para {challenge.identifier}</p>
<input value={code} onChange={(e) => setCode(e.target.value)} inputMode="numeric" />
</form>
);
}challenge.identifier chega mascarado — u***@example.com — então pode ser
exibido sem risco. O expires_in está lá para uma contagem regressiva.
O que pode rejeitar um cadastro
Verificado contra as checagens da própria plataforma, na ordem em que rodam:
Cadastro fechado403Permitir novos usuários a se inscreverem está desligado neste ambiente. Traz mensagem, sem code.
Captcha400captcha_required quando o token falta, captcha_verification_failed quando o
provedor rejeita. Habilitado por ambiente para cadastros.
Sem e-mail nem telefone400Um dos dois é sempre obrigatório. Só mensagem, sem code.
Credencial obrigatória ausente400Credenciais obrigatórias pode exigir e-mail, telefone ou ambos. Só mensagem — veja a nota abaixo.
Domínio de e-mail bloqueado403O endereço não passou na lista permitida do ambiente. Só mensagem, sem code.
Senha rejeitada400password_weak para tamanho e requisitos de caracteres; password_leaked
quando a checagem de vazamento está ligada e a senha é conhecida. São dois
códigos diferentes, não um.
Já existe422email_already_exists, phone_already_exists ou username_already_exists —
mas só quando exatamente um campo colide. Quando dois colidem de uma vez, a
resposta traz a mensagem e nenhum code.
"Credencial obrigatória ausente" calcula user_missing_email e
user_missing_phone internamente e depois lança sem eles, então nenhum dos dois
códigos chega até você. Trate pelo 400 e pelo seu próprio conhecimento do
Credenciais obrigatórias, em vez de esperar um código que não é enviado.
username sozinho não basta
O SignUpParams aceita username, mas o cadastro sempre exige também um email
ou um phone. Um cadastro só com username é rejeitado.
Cadastros que enviam algo — os caminhos otp e link — também têm limite de
taxa, no mesmo balde dos outros envios de verificação.
As regras de senha são do ambiente, não suas
A senha é verificada contra as regras de Senhas: um tamanho mínimo, requisitos de caracteres e, opcionalmente, uma checagem contra senhas sabidamente vazadas. Não reimplemente essas regras no seu formulário — espelhe no texto de ajuda se quiser, mas deixe a plataforma ser quem rejeita, para os dois nunca discordarem.
Escrevendo a ramificação uma vez
O objetivo é justamente que este código sobreviva a um ambiente ser
reconfigurado de otp para link e de volta:
const { data, error } = await nulvless.auth.signUp({ email, password, profile });
if (error) return showError(error);
if (data.challenge?.preferred === 'link') return setCheckYourEmail(true);
if (data.challenge) return router.push('/verify', { state: { challenge: data.challenge } });
return router.push('/sign-in');Três linhas, e nenhuma delas menciona em qual modo o ambiente está.