Convites
O ciclo completo de trazer alguém para dentro de um recurso, status por status.
Um convite é um grant que existe antes de estar ativo. Essa única ideia explica o resto desta página: não há tabela separada de convites, não há estado de "usuário pendente", e não há nada para reconciliar quando a pessoa finalmente aceita.
O status é o ciclo de vida
Todo grant carrega um:
invitedCriado pelo invite(). Ainda não ativo.requestedA pessoa pediu acesso. Ainda não ativo.grantedAtivo. É o que o verify() honra.blockedO registro sobrevive, o acesso não.Só grants granted passam numa verificação de permissão. Um grant invited é
inerte — emitir um não entrega nada até ser aceito.
Um verify contra um grant ainda invited ou requested responde 403 com o
código forbidden_access_waiting_accept, e não com o forbidden_resource_access
genérico. Vale tratar os dois de formas diferentes na interface.
Enviando
Convites saem em lote, contra um recurso:
import { forwardUserHeaders } from '@nulvless/nulvless-js';
const { data: grants } = await nulvless.auth.grants.invite(
{
invites: [
{ email: '[email protected]', role_id: adminRoleId },
{ email: '[email protected]', role_id: memberRoleId },
],
resource: 'acme',
message: 'Acme Inc',
},
{
headers: forwardUserHeaders(req),
redirect_uri: 'https://app.example.com/join',
},
);Três coisas nessa chamada são fáceis de errar:
role_id é um id, não um nome. Leia os ids de um contexto uma vez com
auth.admin.roles.list('org') e mapeie no seu próprio código — 'admin' não é
aceito aqui.
resource, não context. O contexto veio junto com o papel. Você nomeia a
coisa que está sendo compartilhada.
redirect_uri é opção, não parâmetro. Fica no segundo argumento, junto dos
headers.
message é mostrado a quem recebe o convite — na prática, o nome legível do
recurso, já que o resource costuma ser um slug.
Convites funcionam antes da conta existir
Você convida um e-mail, não um id de usuário. O grant é criado com
status: 'invited' e sem granted_to — esse campo é preenchido quando a pessoa
passa a ter conta. Nada no seu código precisa distinguir "convidei um usuário que
já existe" de "convidei um desconhecido".
Repasse os headers do usuário
auth.grants.invite age como o usuário que chamou: ele aplica a permissão dela
sobre o recurso e registra ela como remetente. Sem o
forwardUserHeaders, o convite parece ter vindo da sua
infraestrutura.
Aceitando
Quem recebe segue o link até o seu redirect_uri, que carrega o id do grant. Sua
aplicação chama:
await nulvless.auth.grants.accept(grantId, {
headers: forwardUserHeaders(req),
});O status passa para granted e o acesso começa. Recusar tem o mesmo formato:
await nulvless.auth.grants.cancel(grantId, { headers: forwardUserHeaders(req) });Aceitar não exige permissão no recurso
Essa é a parte que costuma ser super-projetada. Aceitar o próprio convite não pode exigir um papel no recurso ao qual você está entrando — você ainda não tem nenhum. O id do grant é a autorização. Proteja essas rotas só com autenticação, não com verificação de permissão.
Vale enunciar essa assimetria com clareza:
| Operação | Exige papel no recurso |
|---|---|
invite | Sim — normalmente owner ou admin |
revoke, block, update | Sim |
accept, cancel, leave | Não — só autenticação |
A tela de membros
getMembers devolve os quatro grupos já separados, então a tela não faz
agrupamento nenhum:
const { data } = await nulvless.auth.grants.getMembers('acme');
data.granted; // membros ativos
data.invitations; // enviados, aguardando resposta
data.requesteds; // pediram para entrar, aguardando aprovação
data.blockeds; // bloqueadosRemovendo alguém
Três finais diferentes, e eles não são intercambiáveis:
await nulvless.auth.grants.revoke({ id }); // remove o grant
await nulvless.auth.grants.block({ id }); // mantém o registro, nega o acesso
await nulvless.auth.grants.leave({ id }); // o usuário se remove- revoke apaga a relação. Use quando o acesso foi concedido por engano, ou a pessoa simplesmente saiu.
- block mantém o registro de que essa pessoa esteve aqui e agora está negada — o histórico por trás das ações passadas dela continua resolvendo.
- leave é o caminho self-service, e não exige permissão no recurso.
Exige sudo
invite, revoke, block e update são protegidos por sudo. Comece com
auth.sessions.sudo() de qualquer forma — é ele que envia o código. Depois gaste
esse código na sessão inteira, ou só nesta chamada:
// Só esta chamada. O `nonce` é o código que o usuário digitou.
await nulvless.auth.grants.revoke({ id, nonce: code });Repare que vai dentro dos params, não nas options.
Veja Modo sudo.
Mudando o que alguém tem
await nulvless.auth.grants.update(grantId, {
scopes: ['projects:read'],
});Isso muda um grant. Para mudar o que todo mundo com um papel pode fazer, edite o papel — todos os portadores se movem de uma vez, e nenhum grant é tocado:
await nulvless.auth.admin.roles.update(roleId, {
scopes: ['members:read', 'members:write', 'projects:read'],
});Recorrer ao grants.update quando você queria roles.update é como modelos de
permissão viram um punhado de exceções por pessoa que ninguém consegue auditar.