Referência OpenAPI: Identidade (canverly-auth)
Esta página é gerada no build a partir de
api/openapi.yamlno repositóriocanverly-auth. Não edite à mão: a fonte é a especificação.
- Versão da especificação:
0.1.0 - Total de operações: 45
- 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).
Operações
Seção intitulada “Operações”GET /health
Seção intitulada “GET /health”Liveness
Autenticação: nenhuma declarada
Respostas
Seção intitulada “Respostas”| Status | Descrição |
|---|---|
200 |
alive |
GET /ready
Seção intitulada “GET /ready”Readiness — pings deps
Autenticação: nenhuma declarada
Respostas
Seção intitulada “Respostas”| Status | Descrição |
|---|---|
200 |
ready |
503 |
error |
GET /.well-known/openid-configuration
Seção intitulada “GET /.well-known/openid-configuration”OIDC discovery
Autenticação: nenhuma declarada
Respostas
Seção intitulada “Respostas”| Status | Descrição |
|---|---|
200 |
ok |
GET /.well-known/jwks.json
Seção intitulada “GET /.well-known/jwks.json”JWKS (EdDSA)
Autenticação: nenhuma declarada
Respostas
Seção intitulada “Respostas”| Status | Descrição |
|---|---|
200 |
ok |
POST /auth/signup
Seção intitulada “POST /auth/signup”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
Corpo da requisição
Seção intitulada “Corpo da requisição”Tipos de conteúdo: application/json (object)
Respostas
Seção intitulada “Respostas”| 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 |
GET /auth/sites/{site_id}/invitations
Seção intitulada “GET /auth/sites/{site_id}/invitations”Convites pendentes daquele site
Autenticação: bearerAuth
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
site_id |
path | sim | — |
Respostas
Seção intitulada “Respostas”| 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. |
POST /auth/sites/{site_id}/invitations
Seção intitulada “POST /auth/sites/{site_id}/invitations”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
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
site_id |
path | sim | — |
Corpo da requisição
Seção intitulada “Corpo da requisição”Tipos de conteúdo: application/json (object)
Respostas
Seção intitulada “Respostas”| Status | Descrição |
|---|---|
202 |
convite criado e e-mail despachado |
400 |
error |
404 |
error |
501 |
error |
DELETE /auth/sites/{site_id}/invitations/{id}
Seção intitulada “DELETE /auth/sites/{site_id}/invitations/{id}”Revogar convite pendente daquele site
Autenticação: bearerAuth
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
site_id |
path | sim | — |
id |
path | sim | — |
Respostas
Seção intitulada “Respostas”| Status | Descrição |
|---|---|
204 |
revogado |
404 |
error |
501 |
error |
GET /auth/orgs/{org_id}/invitations
Seção intitulada “GET /auth/orgs/{org_id}/invitations”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
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
org_id |
path | sim | — |
Respostas
Seção intitulada “Respostas”| 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. |
DELETE /auth/orgs/{org_id}/invitations/{id}
Seção intitulada “DELETE /auth/orgs/{org_id}/invitations/{id}”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
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
org_id |
path | sim | — |
id |
path | sim | — |
Respostas
Seção intitulada “Respostas”| 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. |
POST /auth/orgs/{org_id}/invitations/{id}/resend
Seção intitulada “POST /auth/orgs/{org_id}/invitations/{id}/resend”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
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
org_id |
path | sim | — |
id |
path | sim | — |
Respostas
Seção intitulada “Respostas”| 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
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
site_id |
path | sim | — |
id |
path | sim | — |
Respostas
Seção intitulada “Respostas”| Status | Descrição |
|---|---|
202 |
reenviado |
400 |
error |
404 |
error |
409 |
INVITATION_ALREADY_ACCEPTED — a pessoa já é membro. |
429 |
error |
501 |
error |
GET /auth/sites/{site_id}/members
Seção intitulada “GET /auth/sites/{site_id}/members”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
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
site_id |
path | sim | — |
Respostas
Seção intitulada “Respostas”| Status | Descrição |
|---|---|
200 |
equipe do site |
400 |
error |
404 |
error |
500 |
error |
PATCH /auth/sites/{site_id}/members/{user_id}
Seção intitulada “PATCH /auth/sites/{site_id}/members/{user_id}”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
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
site_id |
path | sim | — |
user_id |
path | sim | — |
Corpo da requisição
Seção intitulada “Corpo da requisição”Tipos de conteúdo: application/json (object)
Respostas
Seção intitulada “Respostas”| 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 |
DELETE /auth/sites/{site_id}/members/{user_id}
Seção intitulada “DELETE /auth/sites/{site_id}/members/{user_id}”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
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
site_id |
path | sim | — |
user_id |
path | sim | — |
Respostas
Seção intitulada “Respostas”| 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 |
POST /auth/magic-link/request
Seção intitulada “POST /auth/magic-link/request”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
Corpo da requisição
Seção intitulada “Corpo da requisição”Tipos de conteúdo: application/json (object)
Respostas
Seção intitulada “Respostas”| Status | Descrição |
|---|---|
202 |
code dispatched |
400 |
error |
429 |
error |
POST /auth/magic-link/verify
Seção intitulada “POST /auth/magic-link/verify”Verify OTP and issue a session
Autenticação: nenhuma declarada
Corpo da requisição
Seção intitulada “Corpo da requisição”Tipos de conteúdo: application/json (object)
Respostas
Seção intitulada “Respostas”| Status | Descrição |
|---|---|
200 |
tokens issued |
401 |
error |
404 |
error |
429 |
error |
POST /auth/login
Seção intitulada “POST /auth/login”Staff login — password + TOTP
Autenticação: nenhuma declarada
Corpo da requisição
Seção intitulada “Corpo da requisição”Tipos de conteúdo: application/json (object)
Respostas
Seção intitulada “Respostas”| 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 |
POST /auth/refresh
Seção intitulada “POST /auth/refresh”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
Corpo da requisição
Seção intitulada “Corpo da requisição”Tipos de conteúdo: application/json (object)
Respostas
Seção intitulada “Respostas”| 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”. |
POST /auth/login/start
Seção intitulada “POST /auth/login/start”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
Corpo da requisição
Seção intitulada “Corpo da requisição”Tipos de conteúdo: application/json (object)
Respostas
Seção intitulada “Respostas”| 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. |
POST /auth/login/mfa/send
Seção intitulada “POST /auth/login/mfa/send”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
Corpo da requisição
Seção intitulada “Corpo da requisição”Tipos de conteúdo: application/json (object)
Respostas
Seção intitulada “Respostas”| 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 |
POST /auth/login/mfa/verify
Seção intitulada “POST /auth/login/mfa/verify”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
Corpo da requisição
Seção intitulada “Corpo da requisição”Tipos de conteúdo: application/json (object)
Respostas
Seção intitulada “Respostas”| 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. |
GET /auth/mfa/methods
Seção intitulada “GET /auth/mfa/methods”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
Respostas
Seção intitulada “Respostas”| Status | Descrição |
|---|---|
200 |
capacidades |
401 |
error |
POST /auth/mfa/passkey/register/begin
Seção intitulada “POST /auth/mfa/passkey/register/begin”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
Respostas
Seção intitulada “Respostas”| 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. |
POST /auth/mfa/passkey/register/finish
Seção intitulada “POST /auth/mfa/passkey/register/finish”Chave de acesso — valida a attestation e persiste
Autenticação: bearerAuth
Corpo da requisição
Seção intitulada “Corpo da requisição”Tipos de conteúdo: application/json (object)
Respostas
Seção intitulada “Respostas”| 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 |
GET /auth/mfa/passkey
Seção intitulada “GET /auth/mfa/passkey”Chaves de acesso da propria conta
Autenticação: bearerAuth
Respostas
Seção intitulada “Respostas”| Status | Descrição |
|---|---|
200 |
lista |
401 |
error |
501 |
error |
DELETE /auth/mfa/passkey/{id}
Seção intitulada “DELETE /auth/mfa/passkey/{id}”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
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
id |
path | sim | — |
X-Step-Up-Code |
header | não | — |
Respostas
Seção intitulada “Respostas”| 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). |
GET /auth/emails
Seção intitulada “GET /auth/emails”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
Respostas
Seção intitulada “Respostas”| Status | Descrição |
|---|---|
200 |
lista |
401 |
error |
501 |
error |
POST /auth/emails
Seção intitulada “POST /auth/emails”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
Corpo da requisição
Seção intitulada “Corpo da requisição”Tipos de conteúdo: application/json (object)
Respostas
Seção intitulada “Respostas”| 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 |
POST /auth/emails/verify
Seção intitulada “POST /auth/emails/verify”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
Corpo da requisição
Seção intitulada “Corpo da requisição”Tipos de conteúdo: application/json (object)
Respostas
Seção intitulada “Respostas”| 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 |
POST /auth/emails/{id}/primary
Seção intitulada “POST /auth/emails/{id}/primary”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
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
id |
path | sim | — |
Corpo da requisição
Seção intitulada “Corpo da requisição”Tipos de conteúdo: application/json (object)
Respostas
Seção intitulada “Respostas”| 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 |
DELETE /auth/emails/{id}
Seção intitulada “DELETE /auth/emails/{id}”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
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
id |
path | sim | — |
Respostas
Seção intitulada “Respostas”| 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 |
POST /auth/password/change
Seção intitulada “POST /auth/password/change”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:
- Bearer valido — prova que ha uma sessao. E o que o sequestrador TEM.
current_password— prova conhecimento da credencial. Fecha o caso do token roubado por quem nao sabe a senha.- 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
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
X-Step-Up-Code |
header | não | — |
Corpo da requisição
Seção intitulada “Corpo da requisição”Tipos de conteúdo: application/json (object)
Respostas
Seção intitulada “Respostas”| 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). |
DELETE /auth/mfa/totp
Seção intitulada “DELETE /auth/mfa/totp”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
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
X-Step-Up-Code |
header | não | — |
Respostas
Seção intitulada “Respostas”| 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. |
POST /auth/mfa/recovery-codes/generate
Seção intitulada “POST /auth/mfa/recovery-codes/generate”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
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
X-Step-Up-Code |
header | não | — |
Corpo da requisição
Seção intitulada “Corpo da requisição”Tipos de conteúdo: application/json (object)
Respostas
Seção intitulada “Respostas”| 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. |
DELETE /auth/sessions
Seção intitulada “DELETE /auth/sessions”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
Parâmetros
Seção intitulada “Parâmetros”| 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 | — |
Respostas
Seção intitulada “Respostas”| 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. |
POST /auth/mfa/phone/start
Seção intitulada “POST /auth/mfa/phone/start”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
Corpo da requisição
Seção intitulada “Corpo da requisição”Tipos de conteúdo: application/json (object)
Respostas
Seção intitulada “Respostas”| 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 |
POST /auth/mfa/phone/confirm
Seção intitulada “POST /auth/mfa/phone/confirm”Confirma o telefone com o codigo recebido
Autenticação: bearerAuth
Corpo da requisição
Seção intitulada “Corpo da requisição”Tipos de conteúdo: application/json (object)
Respostas
Seção intitulada “Respostas”| 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 |
DELETE /auth/mfa/phone
Seção intitulada “DELETE /auth/mfa/phone”Remove o telefone da conta (idempotente)
Autenticação: bearerAuth
Respostas
Seção intitulada “Respostas”| Status | Descrição |
|---|---|
204 |
removido (ou nao havia nada a remover) |
401 |
error |
501 |
error |
GET /auth/transfers/pending
Seção intitulada “GET /auth/transfers/pending”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
Respostas
Seção intitulada “Respostas”| Status | Descrição |
|---|---|
200 |
contagem por organização e total |
401 |
error |
500 |
error |
GET /auth/orgs/{org_id}/transfers
Seção intitulada “GET /auth/orgs/{org_id}/transfers”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
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
org_id |
path | sim | — |
Respostas
Seção intitulada “Respostas”| Status | Descrição |
|---|---|
200 |
transferências entrando/saindo e compartilhamentos |
400 |
error |
401 |
error |
403 |
error |
404 |
error |
500 |
error |
501 |
error |
POST /auth/orgs/{org_id}/transfers
Seção intitulada “POST /auth/orgs/{org_id}/transfers”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
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
org_id |
path | sim | — |
Corpo da requisição
Seção intitulada “Corpo da requisição”Tipos de conteúdo: application/json (object)
Respostas
Seção intitulada “Respostas”| 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 |
POST /auth/orgs/{org_id}/transfers/{id}/accept
Seção intitulada “POST /auth/orgs/{org_id}/transfers/{id}/accept”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
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
org_id |
path | sim | — |
id |
path | sim | — |
Respostas
Seção intitulada “Respostas”| 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 |
POST /auth/orgs/{org_id}/transfers/{id}/reject
Seção intitulada “POST /auth/orgs/{org_id}/transfers/{id}/reject”Recusar uma transferência recebida
A org da URL é o DESTINO. Nada muda no cadastro do site.
Autenticação: bearerAuth
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
org_id |
path | sim | — |
id |
path | sim | — |
Respostas
Seção intitulada “Respostas”| 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 |
POST /auth/orgs/{org_id}/transfers/{id}/cancel
Seção intitulada “POST /auth/orgs/{org_id}/transfers/{id}/cancel”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
Parâmetros
Seção intitulada “Parâmetros”| Nome | Onde | Obrigatório | Descrição |
|---|---|---|---|
org_id |
path | sim | — |
id |
path | sim | — |
Respostas
Seção intitulada “Respostas”| 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 |