Exportação Verificada
O sistema de exportação verificada permite baixar relatórios — incluindo pacotes .sulfite assinados — exigindo que o solicitante confirme sua identidade via código OTP enviado por email.
┌──────────┐ POST /export/verify ┌──────────────┐ email ┌──────────┐
│ Cliente │──────────────────────▶│ Server │──────────▶│ Email │
│ │◀── { status: sent } │ (gera OTP) │ │ (OTP) │
│ │ └──────────────┘ └──────────┘
│ │ POST /export/confirm
│ │ { email, reportId, code, format }
│ │──────────────────────▶┌──────────────┐
│ │◀── .sulfite assinado │ Server │
└──────────┘ │ (verifica + │
│ assina + │
│ gera output)│
└──────────────┘Princípios:
- O OTP é gerado com
Random.secure()— 15 caracteres alfanuméricos por default - O código é armazenado como hash SHA-256 — nunca em plaintext
- A comparação é timing-safe
- Após verificação, o relatório é assinado com
role: "export"e a identidade do usuário é registrada (VerifiedIdentity) - O OTP é consumido na verificação — não pode ser reutilizado
Endpoints
Política de exportação — GET /api/v1/export/policy
Autenticação: canGenerate (opcional — retorna dados genéricos em modo dev)
Retorna a configuração de export policy do servidor. O cliente usa isso para decidir se deve exibir o fluxo OTP.
Response 200:
{
"protectedFormats": ["sulfite", "pdf"],
"requireVerificationForAll": false,
"otpExpirationSeconds": 300,
"identityHints": ["@acme.com", "user@example.com"]
}| Campo | Descrição |
|---|---|
protectedFormats | Formatos que exigem verificação OTP |
requireVerificationForAll | Se true, todos os formatos precisam de OTP |
otpExpirationSeconds | Tempo de vida do código em segundos |
identityHints | Domínios/emails permitidos (para UI de dicas) |
Solicitar OTP — POST /api/v1/export/verify
Autenticação: canGenerate (opcional em modo dev)
Gera e envia um código OTP para o email informado.
Body:
{
"email": "user@acme.com",
"reportId": "uuid-do-relatorio"
}Response 200:
{
"status": "sent",
"expiresIn": 300
}Erros:
| Status | Erro | Quando |
|---|---|---|
400 | invalid_email | Email inválido |
400 | validation_error | reportId ausente |
403 | identity_not_allowed | Email/domínio não permitido por EXPORT_ALLOWED_IDENTITIES |
404 | not_found | Relatório não encontrado |
429 | rate_limited | Código já enviado há menos de 60s, ou limite por hora atingido (5/hora por email) |
503 | otp_store_full | Capacidade do OTP store atingida |
503 | otp_delivery_failed | Falha no envio do email |
503 | not_configured | OTP service não configurado (sem email provider) |
Confirmar OTP e baixar — POST /api/v1/export/confirm
Autenticação: canGenerate (opcional em modo dev)
Verifica o código OTP e retorna o relatório exportado, assinado.
Body:
{
"email": "user@acme.com",
"reportId": "uuid-do-relatorio",
"code": "ABC123XYZ456DEF",
"format": "sulfite"
}| Campo | Obrigatório | Default | Descrição |
|---|---|---|---|
email | Sim | — | Email que recebeu o OTP |
reportId | Sim | — | ID do relatório |
code | Sim | — | Código OTP recebido |
format | Não | sulfite | sulfite, pdf, html, csv, excel |
Response 200: Bytes do relatório exportado.
Headers de resposta:
Content-Type: application/octet-stream (sulfite) / application/pdf (pdf) / ...
Content-Disposition: attachment; filename="nome-do-relatorio.sulfite"
X-Sulfite-Signed: true
X-Sulfite-Signed-By: user@acme.com
X-Sulfite-Signed-At: 2026-04-09T10:00:00.000ZErros:
| Status | Erro | Quando |
|---|---|---|
400 | invalid_email | Email inválido |
400 | validation_error | reportId ou code ausente |
400 | invalid_code | Código incorreto |
400 | otp_not_found | Nenhum código pendente para este email/relatório |
403 | identity_not_allowed | Email/domínio não permitido |
404 | not_found | Relatório não encontrado |
410 | expired | Código expirado |
422 | invalid_report | Relatório sem definição válida |
422 | package_rebuild_failed | Falha ao preservar conteúdo do pacote .sulfite ao assinar |
423 | max_attempts | Tentativas esgotadas — solicitar novo código |
503 | not_configured | OTP service ou signer não configurados |
Configuração
Variáveis de ambiente
| Variável | Default | Descrição |
|---|---|---|
EXPORT_ALLOWED_IDENTITIES | — (todos) | Emails ou domínios permitidos, separados por vírgula. Ex: user@example.com,acme.com |
EXPORT_PROTECTED_FORMATS | sulfite,pdf | Formatos que exigem OTP |
EXPORT_REQUIRE_ALL | false | Se true, todos os formatos exigem OTP |
EXPORT_OTP_EXPIRATION | 300 | Expiração do OTP em segundos |
EXPORT_OTP_MAX_ATTEMPTS | 3 | Tentativas antes de invalidar |
EXPORT_OTP_LENGTH | 15 | Comprimento do código |
Também é necessário configurar um email provider (Resend ou MailerSend). Veja Configuração → Email.
Requisitos
ENCRYPTION_KEYconfigurado (necessário para assinar o relatório após verificação)- Um email provider configurado (
RESEND_API_TOKENouMAILERSEND_API_TOKEN)
Exemplo de configuração
export ENCRYPTION_KEY="base64-encoded-32-byte-key"
export EXPORT_ALLOWED_IDENTITIES="acme.com,partner@example.com"
export EXPORT_PROTECTED_FORMATS="sulfite,pdf"
export EXPORT_OTP_EXPIRATION=300
export RESEND_API_TOKEN="re_..."
export RESEND_FROM_EMAIL="noreply@acme.com"Fluxo completo
1. Cliente detecta a política
GET /api/v1/export/policy
# → { "protectedFormats": ["sulfite", "pdf"], ... }Se o formato desejado está em protectedFormats, exibir o fluxo OTP.
2. Solicitar OTP
curl -X POST https://server.com/api/v1/export/verify \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "email": "user@acme.com", "reportId": "abc123" }'
# Response: { "status": "sent", "expiresIn": 300 }
# → Usuário recebe email com o código3. Confirmar e baixar
curl -X POST https://server.com/api/v1/export/confirm \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "user@acme.com",
"reportId": "abc123",
"code": "ABC123XYZ456DEF",
"format": "sulfite"
}' \
-o relatorio.sulfiteO arquivo baixado é um pacote .sulfite assinado digitalmente com a identidade verificada do usuário.
Assinatura resultante
Após exportação verificada, o relatório carrega uma assinatura com role: "export":
{
"_signatures": {
"export": {
"alg": "hmac-sha256",
"key_id": "current",
"value": "base64url...",
"signed_at": "2026-04-09T10:00:00.000Z",
"origin": "verified-export",
"identity": {
"email": "user@acme.com",
"verified_at": "2026-04-09T10:00:00.000Z",
"domain": "acme.com"
}
}
}
}Essa assinatura pode ser verificada via GET /api/v1/reports/:id/signatures. Veja Assinatura.
Segurança
| Proteção | Detalhe |
|---|---|
| OTP hashing | SHA-256 — código nunca armazenado em plaintext |
| Comparação | Timing-safe (evita timing attacks) |
| Rate limit por IP | Herdado do rate limiter global |
| Rate limit por email | 5 OTPs/hora por email |
| Rate limit de reenvio | 60s entre pedidos para o mesmo relatório |
| Tentativas máximas | 3 (configurável) — após esgotar, OTP invalidado |
| Identidades permitidas | EXPORT_ALLOWED_IDENTITIES — bloqueia emails/domínios não autorizados |
| Auditoria | Todos os eventos logados (ver Segurança → Auditoria) |