Pular para o conteúdo

Referência OpenAPI: Identidade (canverly-auth)

Esta página é gerada no build a partir de api/openapi.yaml no repositório canverly-auth. Não edite à mão: a fonte é a especificação.

  • Versão da especificação: 0.1.0
  • Total de operações: 49
  • Servidores declarados: https://auth.canverly.com/api/v1
  • Especificação publicada (YAML): /specs/auth-openapi.yaml

See [ADRs 0014, 0019, 0020] in camverly-platform. This API is reached via auth.camverly.com (browser) and /api/v1/auth/* through the gateway (service-to-service and SPAs).

Liveness

Autenticação: nenhuma declarada

Status Descrição
200 alive

Readiness — pings deps

Autenticação: nenhuma declarada

Status Descrição
200 ready
503 error

OIDC discovery

Autenticação: nenhuma declarada

Status Descrição
200 ok

JWKS (EdDSA)

Autenticação: nenhuma declarada

Status Descrição
200 ok

Public self-signup (account without organization)

Cria uma conta staff sem organização nenhuma, em status=pending_verification, e dispara um OTP de purpose=email_verify. A conta não opera (não recebe sessão, não alcança recurso de tenant) até confirmar o código em POST /auth/email/verify/confirm.

Anti-enumeração: a resposta é IDÊNTICA (mesmo 202, mesmo corpo) para e-mail novo e para e-mail que já tem conta, e o custo de CPU é o mesmo nos dois casos. Quando o e-mail já existe, o dono da caixa recebe um aviso — é o único canal pelo qual essa informação circula. Nunca devolve 409.

Rate-limit por IP e por e-mail (429).

POLÍTICA DE SENHA (onda 4 do IAM): comprimento mínimo 12 e a senha não pode constar de vazamentos públicos conhecidos (PASSWORD_BREACHED). A verificação usa a API de range do Have I Been Pwned por k-anonimato: só os 5 primeiros caracteres do SHA-1 saem do servidor — a senha nunca sai, nem inteira nem em hash completo. Nenhuma das duas recusas depende da existência da conta, então o contrato anti-enumeração acima continua intacto.

Quando o corpus de vazamentos está inalcançável, a senha é ACEITA com o degrau de comprimento aplicado (fail-open deliberado, logado e medido em auth_password_breach_checks_total{outcome="unavailable"}). Ver a justificativa em application.PasswordPolicy.Validate.

Autenticação: nenhuma declarada

Tipos de conteúdo: application/json (object)

Status Descrição
202 Aceito. Mesmo corpo para e-mail novo e já cadastrado.
400 INVALID_EMAIL, PASSWORD_TOO_WEAK (< 12 caracteres) ou PASSWORD_BREACHED (a senha escolhida consta de listas públicas de vazamento). Os mesmos códigos valem em POST /auth/password/reset/confirm, POST /v1/auth/sso/register e POST /auth/password/change.
429 error
501 error

Convites pendentes daquele site

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
site_id path sim —
Status Descrição
200 lista (somente convites daquele site)
404 error
501 INVITES_DISABLED, em duas posições distintas da escada: * repositório de CONVITES não cabeado → checado DEPOIS da autorização do site (um 501 antes do gate confirmaria que o site existe); sem acesso ao site a resposta continua 404; * fonte de AUTORIZAÇÃO (memberships) não cabeada → checado ANTES do gate, e seguro porque nesse estado TODO chamador recebe a mesma resposta: o serviço não reconhece membro nenhum, então ela não revela nada sobre aquele site em particular.

Convidar um e-mail para UM site (escopo de site)

O aceite concede site_membership naquele site e nunca org_membership — o convidado não alcança os outros sites da organização. O org_id é derivado no servidor a partir do vínculo do site; não existe campo de escopo no corpo.

Autoriza: site_owner do site, ou quem tem members:manage na organização dona daquele site. Sem acesso ao site → 404.

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
site_id path sim —

Tipos de conteúdo: application/json (object)

Status Descrição
202 convite criado e e-mail despachado
400 error
404 error
501 error

Revogar convite pendente daquele site

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
site_id path sim —
id path sim —
Status Descrição
204 revogado
404 error
501 error

Convites pendentes da organização (org + sites dela)

Lista os convites não aceitos e não expirados da empresa, nos dois escopos (scope: org e scope: site) — quem administra membros da org precisa enxergar tudo o que foi prometido em nome dela.

Autoriza em DOIS degraus, com negativas diferentes de propósito: quem não é membro da org recebe 404 (um 403 confirmaria a existência da empresa a quem só chutou um ULID); quem é membro sem members:manage recebe 403.

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
org_id path sim —
Status Descrição
200 lista de convites pendentes (org + sites da org)
400 error
401 error
403 error
404 error
500 error
501 DOIS códigos, em POSIÇÕES DIFERENTES da escada — a diferença é deliberada e é o que impede o 501 de virar oráculo de existência: * INVITES_DISABLED — o módulo de convites não está cabeado. Checado DEPOIS da autorização, porque neste estado EXISTE resposta de membro (a org é reconhecível) e um 501 antes do gate contaria a um estranho que a empresa existe; ele continua com 404. Falha FECHADA e explícita: uma lista vazia (200) faria a tela confundir “não há convite pendente” com “convites desligados”. * AUTHZ_UNAVAILABLE — a fonte de autorização (memberships) não está cabeada. Checado ANTES do gate, e isso é seguro porque a resposta não depende do org_id nem de quem chama: nesse estado o serviço não reconhece membro nenhum, então não há resposta “de membro” da qual a do forasteiro pudesse ser distinguida.

Revogar convite pendente da organização

org_id entra no WHERE — adivinhar o id de um convite de outra empresa não revoga nada (defesa de IDOR).

Mesma autorização em dois degraus da listagem e do reenvio: 404 para quem não é membro da org, 403 para o membro sem members:manage.

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
org_id path sim —
id path sim —
Status Descrição
204 revogado
400 error
401 error
403 error
404 NOT_FOUND — o chamador não é membro da org ou não existe convite pendente com aquele id naquela org. Os dois casos compartilham o status de propósito.
500 error
501 Os mesmos dois códigos da listagem, nas mesmas posições: INVITES_DISABLED (módulo de convites desligado) depois da autorização — antes deste conserto esta rota mentia com 404, e a UI concluía que aquele convite específico havia sumido; e AUTHZ_UNAVAILABLE (memberships não cabeados) antes do gate, que é seguro por não depender do org_id nem de quem chama.

Reenviar o e-mail de um convite da organização

O token é ROTACIONADO: o link anterior deixa de valer no mesmo UPDATE em que o novo nasce. Reenviar mantendo o token velho deixaria dois links vivos com o mesmo segredo — e o motivo mais comum de reenviar é desconfiar do primeiro. A expiração reinicia a partir de agora (InvitationTTL = 7 dias), o que também torna esta a forma de ressuscitar um convite vencido.

A resposta NUNCA devolve o token. Ele vai só para a caixa do convidado.

Alcança tanto o convite de organização quanto os de sites daquela org (o mesmo conjunto que GET /auth/orgs/{org_id}/invitations lista).

Autoriza: membro da org com members:manage. Quem não é da org recebe 404 (não confirma a existência da empresa); quem é da org mas não administra membros recebe 403.

Freado por IP e pela caixa do convidado (429 RATE_LIMITED).

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
org_id path sim —
id path sim —
Status Descrição
202 Reenviado. email_sent:false = o token JÁ rotacionou (o link antigo morreu) e só o envio falhou — reenvie de novo.
400 error
403 error
404 error
409 INVITATION_ALREADY_ACCEPTED — a pessoa já é membro.
429 error
501 error

POST /auth/sites/{site_id}/invitations/{id}/resend

Seção intitulada “POST /auth/sites/{site_id}/invitations/{id}/resend”

Reenviar o e-mail de um convite daquele site

Mesma semântica da rota de organização (token rotacionado, expiração reiniciada, token nunca na resposta), com o escopo do SITE no WHERE: o id de um convite de outro site simplesmente não é encontrado.

Autoriza: site_owner do site, ou members:manage na organização dona dele. Sem acesso ao site → 404 (403 confirmaria que o site existe).

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
site_id path sim —
id path sim —
Status Descrição
202 reenviado
400 error
404 error
409 INVITATION_ALREADY_ACCEPTED — a pessoa já é membro.
429 error
501 error

Equipe daquele site (quem tem site_membership)

Lista quem tem vínculo GRAVADO naquele site. Não inclui quem alcança o site por papel de ORGANIZAÇÃO (projeção): essas pessoas não têm linha em site_memberships e removê-las é operação de empresa, não de site.

Autoriza: site_owner do site, ou members:manage na organização dona dele. Sem acesso ao site → 404.

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
site_id path sim —
Status Descrição
200 equipe do site
400 error
404 error
500 error

Trocar o papel de alguém da equipe do site

É o “promover a dono” que o 409 LAST_SITE_OWNER instrui — sem esta rota, aquela mensagem mandaria o operador por um caminho inexistente.

REBAIXAR o último dono cai na MESMA guarda (409 LAST_SITE_OWNER): trocar o papel do único dono para editor orfana o site exatamente como removê-lo. Rebaixar um dono havendo outro é permitido, e reatribuir o papel de dono ao próprio dono também (não é rebaixamento).

Papéis aceitos: qualquer papel de escopo site que exista na empresa — os presets (site_owner, site_editor, site_author, site_contributor, com os apelidos owner/editor/author/ contributor/dono/autor/colaborador) e os papéis PERSONALIZADOS que a empresa criou (pelo slug). Papel de ORGANIZAÇÃO nunca passa (admin, org_admin, … → 400): a resolução fixa o escopo site, então o vetor de escalada morre ali.

Só o papel ESTRUTURAL (is_system) conta como “continua dono” — um papel personalizado batizado de “dono” não fura a guarda.

Autoriza: site_owner do site, ou members:manage na organização dona dele. Sem acesso ao site → 404 opaco.

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
site_id path sim —
user_id path sim —

Tipos de conteúdo: application/json (object)

Status Descrição
200 papel trocado
400 BAD_ID (user_id inválido), BAD_REQUEST (corpo inválido), BAD_ROLE (role vazio), ROLE_NOT_FOUND (papel de site inexistente nesta empresa — inclui todo papel de organização).
404 NOT_FOUND (sem acesso ao site — opaco) ou NOT_MEMBER (a pessoa não tem vínculo com este site).
409 LAST_SITE_OWNER — o rebaixamento deixaria o site órfão.
500 error
501 error

Remover alguém da equipe do site

Idempotente: remover quem não é membro devolve o MESMO 204 de quem era. Respostas diferentes transformariam o endpoint num oráculo de “essa pessoa tem acesso a este site?”.

Nunca remove o ÚLTIMO dono (409 LAST_SITE_OWNER): um site sem dono não pode mais ser administrado por ninguém. Vale inclusive para quem está tentando sair — auto-remoção é permitida, exceto se você for o último dono.

Falha ao contar os donos ⇒ 500 e nada é removido (falha fechada).

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
site_id path sim —
user_id path sim —
Status Descrição
204 removido (ou já não era membro)
400 error
404 error
409 LAST_SITE_OWNER — deixaria o site órfão.
500 error

Equipe da organização (vínculos de nível de org)

Lista quem tem vínculo de ORGANIZAÇÃO (até 100). Quem só tem vínculo de site dentro da empresa não aparece aqui — está em GET /auth/sites/{site_id}/members.

Autoriza: qualquer membro da organização. Forasteiro → 404 opaco (não confirma que a org existe). Falha ao ler e-mail/nome dos membros ⇒ 500 LOOKUP_FAILED, nunca uma lista com linhas anônimas.

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
org_id path sim —
Status Descrição
200 equipe da organização
400 BAD_ORG — org_id não é ULID.
401 error
404 NOT_FOUND — quem chama não é membro (opaco).
500 error
501 AUTHZ_UNAVAILABLE — serviço sem repositório de vínculos.

Adicionar pessoa à organização (ou convidar quem não tem conta)

Com conta existente: concede o vínculo (201 added) ou, se a pessoa já é membro, troca o papel (200 updated). Sem conta e com convites ligados: cria convite de ORGANIZAÇÃO com o mesmo papel e manda o link (202 invited; email_sent: false = o convite existe e só o envio falhou — reenvie, não duplique). Convites desligados ⇒ 404 USER_NOT_FOUND.

Autoriza: members:manage na organização. Regras do dono (papel estrutural is_system, lido por atributo, nunca pelo nome):

  • conceder o papel de dono exige SER dono ⇒ senão 403 OWNER_ONLY;
  • mexer em quem já é dono exige SER dono ⇒ senão 403 OWNER_PROTECTED;
  • rebaixar o último dono ⇒ 409 LAST_ORG_OWNER (guarda no repositório, na mesma transação, com FOR UPDATE na organização).

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
org_id path sim —

Tipos de conteúdo: application/json (object)

Status Descrição
200 já era membro; papel trocado
201 vínculo concedido
202 e-mail sem conta; convite criado
400 BAD_ORG, BAD_REQUEST, BAD_ROLE, BAD_EMAIL ou ROLE_NOT_FOUND.
401 error
403 FORBIDDEN (sem members:manage), OWNER_ONLY (quem não é dono tentou conceder o papel de dono) ou OWNER_PROTECTED (quem não é dono tentou alterar ou remover um dono).
404 NOT_FOUND (quem chama não é membro, opaco) ou USER_NOT_FOUND (sem conta e convites desligados).
409 LAST_ORG_OWNER — a empresa ficaria sem dono; torne outra pessoa dona antes.
500 error
501 AUTHZ_UNAVAILABLE ou NOT_IMPLEMENTED (papéis desligados).

Trocar o papel de um membro da organização

Autoriza: members:manage. Forasteiro → 404 opaco antes do 403. Dar o papel de dono exige SER dono (403 OWNER_ONLY); mexer em quem é dono exige SER dono (403 OWNER_PROTECTED); rebaixar o último dono ⇒ 409 LAST_ORG_OWNER. Se o titular (organizations.owner_user_id) deixa de ser dono, o registro passa ao dono restante mais antigo.

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
org_id path sim —
user_id path sim —

Tipos de conteúdo: application/json (object)

Status Descrição
200 papel trocado
400 BAD_ID, BAD_REQUEST, BAD_ROLE ou ROLE_NOT_FOUND.
401 error
403 FORBIDDEN (sem members:manage), OWNER_ONLY (quem não é dono tentou conceder o papel de dono) ou OWNER_PROTECTED (quem não é dono tentou alterar ou remover um dono).
404 NOT_FOUND (quem chama não é membro, opaco) ou NOT_MEMBER (o alvo não é membro).
409 LAST_ORG_OWNER — a empresa ficaria sem dono; torne outra pessoa dona antes.
500 error
501 AUTHZ_UNAVAILABLE ou NOT_IMPLEMENTED (papéis desligados).

Remover um membro da organização

Autoriza: members:manage. Remover um dono exige SER dono (403 OWNER_PROTECTED); inclusive a si mesmo, desde que sobre outro dono. O último dono nunca sai ⇒ 409 LAST_ORG_OWNER.

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
org_id path sim —
user_id path sim —
Status Descrição
204 removido
400 BAD_ID — org_id ou user_id não é ULID.
401 error
403 FORBIDDEN (sem members:manage), OWNER_ONLY (quem não é dono tentou conceder o papel de dono) ou OWNER_PROTECTED (quem não é dono tentou alterar ou remover um dono).
404 NOT_FOUND — quem chama não é membro (opaco).
409 LAST_ORG_OWNER — a empresa ficaria sem dono; torne outra pessoa dona antes.
500 error
501 AUTHZ_UNAVAILABLE — serviço sem repositório de vínculos.

Send OTP to email

Anti-enumeração: um e-mail sem conta recebe o MESMO 202 de um e-mail com conta (antes: 404 USER_NOT_FOUND para propósitos que exigem conta, o que revelava quem tem cadastro). O desfecho real fica no log do servidor. Freado por IP e por e-mail.

Autenticação: nenhuma declarada

Tipos de conteúdo: application/json (object)

Status Descrição
202 code dispatched
400 error
429 error

Verify OTP and issue a session

Autenticação: nenhuma declarada

Tipos de conteúdo: application/json (object)

Status Descrição
200 tokens issued
401 error
404 error
429 error

Staff login — password + TOTP

Autenticação: nenhuma declarada

Tipos de conteúdo: application/json (object)

Status Descrição
200 tokens issued
401 error
403 EMAIL_NOT_VERIFIED — conta de auto-cadastro que ainda não confirmou o e-mail. Só é devolvido DEPOIS de a senha bater, para não virar oráculo de enumeração.
423 error

Rotate refresh token

ROTACIONA: a sessao antiga e revogada e outra e emitida. Apresentar o MESMO refresh token duas vezes e tratado como token roubado e revoga TODAS as sessoes do usuario — quem chama precisa de single-flight (uma renovacao em voo por navegador).

O token pode vir no CORPO (BFF, server-side) ou no cookie camverly_refresh (navegador, onde o cookie e HttpOnly e o JavaScript nao consegue monta-lo). Corpo AUSENTE e tolerado; corpo QUEBRADO e 400.

Autenticação: nenhuma declarada

Tipos de conteúdo: application/json (object)

Status Descrição
200 new tokens
400 BAD_REQUEST — corpo PRESENTE e malformado. NAO significa sessao perdida: e defeito de serializacao do cliente.
401 REFRESH_FAILED — a sessao acabou de verdade (token desconhecido, revogado, reuso detectado, usuario inexistente). E o UNICO status que significa “mande o usuario ao login”.
403 error
423 error
503 REFRESH_UNAVAILABLE — nao foi possivel DETERMINAR se a sessao vale (banco fora, timeout). O cliente deve tentar de novo e nunca deslogar: “nao sei” nao e “nao vale”.

Login faseado, passo 1 — prova a senha e abre o desafio

Devolve um desafio opaco e a lista de segundos fatores DISPONIVEIS para aquela conta. methods so existe no 200: publicar o inventario de fatores para quem nao provou a senha entregaria parte do e-mail e do telefone da vitima.

O 401 e IDENTICO em corpo, status e TEMPO entre “e-mail nao existe” e “senha errada” (uma derivacao Argon2id equivalente e queimada nos caminhos que recusam antes da comparacao).

Autenticação: nenhuma declarada

Tipos de conteúdo: application/json (object)

Status Descrição
200 desafio aberto
400 error
401 INVALID_CREDENTIALS — resposta e tempo uniformes.
423 error
428 MFA_NOT_ENROLLED — staff sem fator TOTP confirmado (ADR-0019, politica preservada). So alcancavel DEPOIS de a senha bater.

Login faseado, passo 2 — entrega o desafio do 2o fator

email e sms DESPACHAM um codigo e respondem 202 com so a expiracao (nada sobre a conta, o endereco ou o numero). totp nao passa por aqui (o segredo ja esta no aparelho).

passkey NAO ENVIA NADA: ele ENTREGA as opcoes da cerimonia e responde 200 com corpo. O objeto publicKey e o argumento de navigator.credentials.get(); challenge e allowCredentials[].id vem em base64url sem padding e precisam virar ArrayBuffer no cliente (base64 comum NAO serve). O desafio fica no SERVIDOR e nao volta no passo 3.

Autenticação: nenhuma declarada

Tipos de conteúdo: application/json (object)

Status Descrição
200 So para method=passkey: opcoes da cerimonia de autenticacao.
202 codigo despachado (email / sms)
400 BAD_REQUEST ou METHOD_NOT_AVAILABLE (canal indisponivel para a conta).
401 CHALLENGE_INVALID — inexistente, expirado ou ja consumido (um codigo so).
501 PASSKEY_DISABLED — WebAuthn nao configurado neste ambiente.
502 error

Login faseado, passo 3 — troca o desafio por uma sessao

Consumo de USO UNICO por compare-and-set. trust_device pede a sessao de 90 dias; o campo trusted_device da resposta diz o que o SERVIDOR decidiu — a tela so promete 90 dias se ele vier true.

Autenticação: nenhuma declarada

Tipos de conteúdo: application/json (object)

Status Descrição
200 sessao emitida (+ cookies camverly_session e camverly_refresh)
400 error
401 MFA_INVALID_CODE (com details.attempts_remaining), CHALLENGE_INVALID, PASSKEY_CEREMONY_INVALID (cerimonia inexistente, expirada ou JA CONSUMIDA — peca outra em /auth/login/mfa/send) ou PASSKEY_CLONE_SUSPECTED (contador de assinaturas REGREDIU; a acao certa e trocar a chave, nao tentar de novo).
429 TOO_MANY_ATTEMPTS — 5a tentativa errada; o desafio MORRE aqui.

Capacidades de 2o fator da propria conta

Existe para a tela de seguranca nao ter de adivinhar se o SMS esta ligado — adivinhar erraria para o lado de mostrar um botao que da 501.

Autenticação: bearerAuth

Status Descrição
200 capacidades
401 error

Chave de acesso — abre a cerimonia de cadastro

O dono vem do Bearer verificado, NUNCA do corpo. O desafio e sorteado e guardado no servidor (uso unico); pedir de novo INVALIDA o pedido anterior. Corpo da requisicao vazio ({}).

Autenticação: bearerAuth

Status Descrição
200 opcoes da cerimonia
401 error
409 PASSKEY_LIMIT_REACHED — teto de 20 chaves por conta.
501 PASSKEY_DISABLED — WebAuthn nao configurado neste ambiente.

Chave de acesso — valida a attestation e persiste

Autenticação: bearerAuth

Tipos de conteúdo: application/json (object)

Status Descrição
201 chave cadastrada
400 BAD_REQUEST — corpo ilegivel ou credential ausente.
401 PASSKEY_CEREMONY_INVALID (nao havia cerimonia viva, ou ela ja foi queimada — recomece pelo /begin) ou PASSKEY_INVALID (attestation recusada: desafio, RP ID, origem, flags ou assinatura).
409 PASSKEY_ALREADY_REGISTERED — esse credential_id ja existe (um codigo so, seja sua ou de outra conta).
501 error

Chaves de acesso da propria conta

Autenticação: bearerAuth

Status Descrição
200 lista
401 error
501 error

Remove uma chave de acesso da propria conta (exige step-up)

Escopado ao dono no proprio DELETE ... AND user_id = $2. Id de outra conta devolve 404, o MESMO codigo de “nao existe”, para o endpoint nao virar oraculo.

STEP-UP OBRIGATORIO desde a onda 4 do IAM — ver o bloco “STEP-UP EM OPERACAO SENSIVEL” mais abaixo neste arquivo. Sem X-Step-Up-Code, responde 428 STEP_UP_REQUIRED e manda um OTP para o e-mail primario (nunca para a passkey que esta sendo removida: quem perdeu a chave fisica precisa exatamente desta operacao).

O step-up vem ANTES de qualquer consulta ao repositorio — emitir o codigo so depois de descobrir que a passkey existe faria a presenca do e-mail virar oraculo de “esse id existe nesta conta?”.

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
id path sim —
X-Step-Up-Code header não —
Status Descrição
204 removida
401 CODE_INVALID (step-up errado) ou nao autenticado.
404 PASSKEY_NOT_FOUND — inexistente, id ilegivel OU de outra conta.
409 LAST_STRONG_FACTOR — a remocao deixaria a conta SEM nenhum fator forte (sem TOTP confirmado e sem outra passkey). O piso do IAM proibe que a troca de fator seja silenciosa.
428 STEP_UP_REQUIRED — details:{channel,sent_to} mascarado.
501 error
503 STEP_UP_UNAVAILABLE — a remocao NAO aconteceu (falha fechada).

E-mails da propria conta (+ nudge de secundario)

O dono vem SEMPRE do Bearer verificado. nudge e decidido no SERVIDOR; a tela nao recalcula a politica de recuperacao.

Autenticação: bearerAuth

Status Descrição
200 lista
401 error
501 error

Adiciona um e-mail e dispara o codigo que o prova

202, nao 201: o que interessa (o endereco virar identidade) ainda NAO aconteceu — a linha nasce PENDENTE e so o /verify a torna utilizavel.

Nao existe resposta “esse e-mail ja pertence a alguem”. Responde-la transformaria o endpoint num oraculo de “essa pessoa tem conta na Canverly?” para qualquer um com uma sessao. A recusa acontece na VERIFICACAO, onde quem a recebe ja provou controlar a caixa.

A linha PENDENTE pode coexistir em varias contas (anti-squatting): se declarar bastasse para reservar, eu bloquearia o endereco de qualquer pessoa so afirmando que e meu.

Freado por IP, por ALVO (3/h no mesmo endereco) e por AUTOR (10/h por conta) — sem isso, este e o caminho mais curto para transformar a plataforma numa maquina de spam com o nosso dominio de envio.

Autenticação: bearerAuth

Tipos de conteúdo: application/json (object)

Status Descrição
202 codigo enviado
400 INVALID_EMAIL / BAD_REQUEST.
401 error
409 EMAIL_ALREADY_ADDED — o endereco ja esta NESTA conta. EMAIL_LIMIT_REACHED — teto de 10 enderecos por conta.
429 RATE_LIMITED.
501 error

Prova o endereco e o transforma em identidade da conta

A partir do 200 o endereco serve para entrar e para recuperar — e a unica transicao desta onda que muda o que a conta consegue fazer.

O codigo tem proposito email_add, TTL de 10 min, 5 tentativas e e de USO UNICO (CAS no banco). Todo desfecho que dependa do codigo colapsa no MESMO 401 CODE_INVALID — inexistente, nao confere, ja usado e tentativas estouradas sao indistinguiveis de proposito.

Autenticação: bearerAuth

Tipos de conteúdo: application/json (object)

Status Descrição
200 verificado
401 CODE_INVALID — um codigo so para todos os desfechos do OTP.
404 EMAIL_NOT_FOUND — esse endereco nao esta nesta conta.
409 EMAIL_ALREADY_ADDED — ja estava verificado (nenhum codigo foi queimado). EMAIL_TAKEN — outra conta provou o mesmo endereco antes; quem prova primeiro fica com ele. Dizer o motivo aqui nao vaza nada: quem chega neste ponto acabou de conferir um codigo, isto e, controla a caixa.
429 RATE_LIMITED.
501 error

Torna um e-mail VERIFICADO o principal (exige step-up)

PROTOCOLO EM DUAS CHAMADAS. Sem code, o servidor emite um OTP (sensitive_action) para o e-mail primario ATUAL e responde 428 STEP_UP_REQUIRED com details.sent_to MASCARADO; nada muda. Com o code conferido, a troca acontece e responde 200.

Por que step-up: quem troca o primario passa a receber a recuperacao da conta. O Bearer sozinho prova “esta sessao esta viva”, que e exatamente o que um sequestrador com sessao roubada tem. O que ele NAO tem e a caixa primaria ATUAL — por isso o codigo vai para o endereco que esta PERDENDO o posto, nunca para o que esta ganhando.

Endereco NAO verificado e recusado com 409 antes do step-up: emitir codigo para uma operacao ja impossivel gastaria OTP a toa.

Idempotente: promover quem ja e primario responde 200 sem queimar codigo.

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
id path sim —

Tipos de conteúdo: application/json (object)

Status Descrição
200 promovido
401 CODE_INVALID (codigo de step-up errado) ou nao autenticado.
404 EMAIL_NOT_FOUND — inexistente, id ilegivel OU de outra conta.
409 EMAIL_NOT_VERIFIED — confirme o endereco antes de torna-lo principal.
428 STEP_UP_REQUIRED — codigo enviado ao primario ATUAL. details traz channel: "email" e sent_to MASCARADO, para a tela dizer para onde o codigo foi sem publicar o endereco.
501 error

Remove um e-mail da propria conta

Escopado ao dono no proprio SQL. Id de outra conta devolve 404, o MESMO codigo de “nao existe”, para nao virar oraculo.

Duas recusas, e cada uma protege uma coisa diferente: o PRIMARIO (a conta ficaria sem canal de recuperacao definido — promova outro antes) e o ULTIMO VERIFICADO (a conta ficaria IRRECUPERAVEL).

Sem step-up, e isso e uma decisao: remover nao move o canal de recuperacao para lugar nenhum, e exigir a caixa primaria travaria a limpeza de quem esta justamente perdendo o acesso a um endereco antigo.

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
id path sim —
Status Descrição
204 removido
401 error
404 EMAIL_NOT_FOUND — inexistente, id ilegivel OU de outra conta.
409 EMAIL_IS_PRIMARY — promova outro antes. EMAIL_LAST_VERIFIED — a conta ficaria irrecuperavel.
501 error

Troca a senha COM SESSAO VIVA (exige senha atual + step-up)

ROTA NOVA. Ate esta onda so existiam duas escritas de senha para quem ja tem conta — o reset por OTP e o cadastro — e nenhuma para “estou logado e quero trocar minha senha”. Nao confundir com POST /auth/password/reset/confirm, que e publico.

TRES PROVAS, nenhuma redundante:

  1. Bearer valido — prova que ha uma sessao. E o que o sequestrador TEM.
  2. current_password — prova conhecimento da credencial. Fecha o caso do token roubado por quem nao sabe a senha.
  3. Step-up por e-mail primario — prova controle da CAIXA. Fecha o caso em que o atacante sabe a senha (reuso, vazamento de outro servico) E tem a sessao, que e quando 1 e 2 sao inuteis.

ORDEM DAS CHECAGENS: politica da senha nova -> senha atual -> step-up. Reprovar depois de queimar o OTP deixaria a pessoa sem codigo e sem senha nova; e exigir a senha atual antes do step-up impede que um token roubado bombardeie a caixa da vitima com e-mails de confirmacao.

A senha nova passa pela MESMA politica do cadastro: comprimento (12) + corpus de senhas vazadas (PASSWORD_BREACHED).

Efeito colateral: no sucesso, TODAS as outras sessoes da conta sao revogadas; a do chamador e poupada. Trocar a senha e quase sempre a reacao a uma suspeita de comprometimento, e uma senha nova que deixa a sessao do invasor viva nao resolve nada.

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
X-Step-Up-Code header não —

Tipos de conteúdo: application/json (object)

Status Descrição
204 senha trocada; as OUTRAS sessoes foram revogadas
400 PASSWORD_TOO_WEAK — menos de 12 caracteres. PASSWORD_BREACHED — a senha escolhida consta de vazamentos publicos.
401 INVALID_CREDENTIALS (senha atual errada) ou CODE_INVALID (step-up).
409 PASSWORD_NOT_SET — conta sem senha (leitor de magic link).
428 STEP_UP_REQUIRED — ver o bloco de step-up acima.
501 error
503 STEP_UP_UNAVAILABLE — o codigo nao pode ser enviado; a troca NAO aconteceu. PASSWORD_CHECK_UNAVAILABLE so ocorre com HIBP_FAIL_CLOSED=true (nao e o padrao).

Remove o fator TOTP da propria conta (exige step-up)

STEP-UP OBRIGATORIO (ver o bloco acima). Derrubar o segundo fator da vitima e o primeiro movimento de quem roubou uma sessao e quer manter o acesso depois que o roubo for descoberto.

O codigo vai para o e-mail primario, nunca para o TOTP que esta sendo removido — quem perdeu o celular precisa exatamente desta operacao.

Idempotente: sem fator cadastrado, responde 204 mesmo assim.

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
X-Step-Up-Code header não —
Status Descrição
204 fator removido
401 CODE_INVALID (step-up) ou nao autenticado.
428 STEP_UP_REQUIRED.
503 STEP_UP_UNAVAILABLE — a remocao NAO aconteceu.

Gera um pool NOVO de codigos de recuperacao (exige step-up)

STEP-UP OBRIGATORIO (ver o bloco acima), e esta e a mais sorrateira das operacoes protegidas: gerar codigos novos INVALIDA os antigos e devolve os novos em texto puro na resposta. Uma unica chamada com sessao roubada entrega ao atacante dez credenciais de recuperacao permanentes (que sobrevivem a troca de senha e a revogacao de sessao) e queima as da vitima, que so descobre no dia em que precisar de uma.

Os codigos aparecem UMA vez; o servidor guarda so o SHA-256.

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
X-Step-Up-Code header não —

Tipos de conteúdo: application/json (object)

Status Descrição
200 pool novo (os codigos NAO sao recuperaveis depois)
401 CODE_INVALID (step-up) ou nao autenticado.
428 STEP_UP_REQUIRED.
501 error
503 STEP_UP_UNAVAILABLE — nenhum codigo foi gerado nem invalidado.

Sair de todas as sessoes (exige step-up)

STEP-UP OBRIGATORIO nas DUAS variantes (ver o bloco acima). E destrutivo e e literalmente o movimento de um invasor que quer expulsar o dono da conta. A variante ?keep_current=1 e a PIOR das duas para esse fim: ela mata todo mundo MENOS quem pediu — ele fica, a vitima sai, e a vitima nem consegue voltar para revogar a sessao dele.

DELETE /auth/sessions/{id} (revogar UMA) continua SEM step-up, e isso e uma decisao com risco residual assumido: quem acabou de perder o celular precisa matar aquela sessao AGORA, e um invasor que ja leu a lista consegue o mesmo efeito em N chamadas. O gate encarece o botao de uma tacada so, que e o que a tela oferece e o que um script usa.

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
keep_current query não mantem a sessao do chamador; TAMBEM exige step-up
X-Step-Up-Code header não —
Status Descrição
204 sessoes revogadas (cookies limpos quando a atual caiu)
401 CODE_INVALID (step-up) ou nao autenticado.
428 STEP_UP_REQUIRED.
503 STEP_UP_UNAVAILABLE — nenhuma sessao foi revogada.

Cadastra um telefone e dispara o codigo de confirmacao

O numero NAO entra na conta aqui: fica pendente ate ser provado, porque telefone nao confirmado nao pode virar segundo fator. Validacao E.164 acontece ANTES de qualquer chamada ao provedor.

Autenticação: bearerAuth

Tipos de conteúdo: application/json (object)

Status Descrição
202 codigo despachado
400 INVALID_PHONE — nao e E.164 aceitavel.
401 error
501 SMS_DISABLED — sem provedor configurado (estado atual).
502 error

Confirma o telefone com o codigo recebido

Autenticação: bearerAuth

Tipos de conteúdo: application/json (object)

Status Descrição
200 telefone verificado
401 MFA_INVALID_CODE com details.attempts_remaining.
404 PHONE_VERIFICATION_NOT_FOUND — nada pendente ou ja expirou.
429 error
501 error

Remove o telefone da conta (idempotente)

Autenticação: bearerAuth

Status Descrição
204 removido (ou nao havia nada a remover)
401 error
501 error

Quantas transferências aguardam decisão do usuário, por organização

Contador de menu do painel: para cada org em que o chamador pode decidir (members:manage), quantas transferências estão ENTRANDO. Serviço sem o módulo cabeado responde {"orgs": [], "total": 0} com 200, e não erro: derrubar a lateral inteira do painel por um enfeite de menu trocaria a ausência de aviso por uma tela quebrada.

Erro ao CONTAR, porém, nunca vira zero — “não consegui contar” e “não há nada” são coisas diferentes, e o painel precisa poder dizer a primeira.

Autenticação: bearerAuth

Status Descrição
200 contagem por organização e total
401 error
500 error

Transferências pendentes da organização (entrando e saindo)

Separa o que ENTRA do que SAI e devolve o NOME das organizações dos dois lados, por extenso. O id não diz nada a quem lê a tela — “aceitar a transferência de 01KVXQ1C…” é um pedido que ninguém deveria aprovar. Traz junto os compartilhamentos (share) cedidos e recebidos.

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
org_id path sim —
Status Descrição
200 transferências entrando/saindo e compartilhamentos
400 error
401 error
403 error
404 error
500 error
501 error

Iniciar transferência (ou compartilhamento) de um site

A org da URL é a ORIGEM; o destino vem por to_org_slug. mode distingue transfer (o site muda de dono) de share (a outra empresa ganha acesso com o papel de share_role, e o dono não muda).

A transferência criada fica pendente até o destino aceitar; ela expira sozinha (expires_at).

CUIDADO OPERACIONAL, medido em 2026-09-05: aceitar a transferência muda o dono no cadastro do site (tenancy), e o ACERVO (posts, mídia, termos, licença) só acompanha porque o tenancy converge os demais serviços numa fila. Se essa convergência estiver parada, o site aparece na empresa nova com o conteúdo ainda carimbado para a antiga.

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
org_id path sim —

Tipos de conteúdo: application/json (object)

Status Descrição
200 transferência criada, pendente de decisão do destino
400 BAD_REQUEST (JSON ou site_id inválido), SAME_ORG (destino igual à origem) ou MODE_INVALID (modo/papel fora do contrato).
401 error
403 FORBIDDEN (membro sem members:manage) ou NOT_OWNER — o site informado não pertence à organização da URL. Adivinhar o ULID do site de outra empresa não transfere nada.
404 NOT_FOUND (não é membro da org) ou TARGET_NOT_FOUND (slug de destino inexistente).
409 TRANSFER_EXISTS — já há uma pendente para essa organização.
500 error
501 error

Aceitar uma transferência recebida

A org da URL é o DESTINO. Aceitar muda o dono do site no tenancy e dispara a convergência do acervo nos demais serviços.

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
org_id path sim —
id path sim —
Status Descrição
200 transferência aceita
400 error
401 error
403 FORBIDDEN — sua organização não é parte desta transferência.
404 NOT_FOUND (não é membro da org) ou TRANSFER_NOT_FOUND.
409 NOT_PENDING — já decidida ou expirada.
500 error
501 error

Recusar uma transferência recebida

A org da URL é o DESTINO. Nada muda no cadastro do site.

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
org_id path sim —
id path sim —
Status Descrição
204 recusada
400 error
401 error
403 FORBIDDEN — sua organização não é parte desta transferência.
404 NOT_FOUND (não é membro da org) ou TRANSFER_NOT_FOUND.
409 NOT_PENDING — já decidida ou expirada.
500 error
501 error

Cancelar uma transferência que a organização iniciou

A org da URL é a ORIGEM. Só cabe enquanto a transferência está pendente — depois de aceita, desfazer é uma transferência nova no sentido contrário.

Autenticação: bearerAuth

Nome Onde Obrigatório Descrição
org_id path sim —
id path sim —
Status Descrição
204 cancelada
400 error
401 error
403 FORBIDDEN — sua organização não é parte desta transferência.
404 NOT_FOUND (não é membro da org) ou TRANSFER_NOT_FOUND.
409 NOT_PENDING — já decidida ou expirada.
500 error
501 error