← Índice da documentação · STATUS do projeto · Decisões · README do projeto
Escopo: contexto Surfaces [AGPL núcleo]. API RESTful pública e servidor MCP sobre os mesmos use-cases (nunca duplicar regra). Segue a direção do Postiz (núcleo AGPL) em: MCP sobre o core, OAuth de recurso protegido, origem da mutação auditada. Corrige: JWT eterno, API key sem hash/escopo. Depende de: SPEC_BACKEND (use-cases/OpenAPI), SPEC_DATA (api_keys, oauth_*, audit_log).
Estado de verdade (2026-07-26): auth humana no runtime é Clerk-only (OpenSpec
clerk-human-authentication). Trechos que ainda descrevam JWT access/refresh Manypost ou rotasGOOGLE_*/GITHUB_*na API são histórico pré-Clerk e não o comportamento vigente — confirme emapps/api/src/http/middleware/auth.ts,openspec/specs/e STATUS.md. Este arquivo mistura requisitos históricos/aspiracionais com o runtime; em conflito, prevalecem código + OpenSpec vivo.
flowchart TB
REST[REST /v1 e /public/v1] --> UC[application use-cases]
MCP[MCP server /mcp] --> UC
WEB[web app] --> REST
subgraph authn [Autenticação unificada]
CLERK[Clerk session JWT - humanos]
KEY[API keys mp_live_ - máquinas]
OAT[OAuth mpo_ - MCP/apps de terceiros]
end
authn --> REST & MCP
UC --> AUD[audit_log central com origem WEB/API/MCP]
Uma única pilha de autorização: qualquer credencial resolve para um Principal {orgId, actorType, actorId, scopes[]}; políticas por endpoint/tool avaliam escopos sobre o Principal — mesmo código para REST e MCP.
- Sessão no Clerk (senha, verificação de e-mail, Google/MFA e demais IdPs no Clerk Dashboard — não há rotas
GOOGLE_*/GITHUB_*na API Manypost). - Cada request humana envia
Authorization: Bearer <Clerk JWT>(ou cookie__sessionno SSE, onde o EventSource não manda header). - A API verifica o token (JWKS / chave local de E2E) e resolve usuário + organização + papel no Postgres (
auth_identities.provider=clerk). Claims de tenant do cliente não autorizam. - Superfície humana REST: basicamente
GET /v1/auth/me+ demais/v1/*com papel (MEMBER/ADMIN). Sem JWT access/refresh Manypost, sem cookie de sessão Manypost, sem exchange. - Fail-closed sem chaves Clerk configuradas (exceto
MODE=worker). - Tabela
sessionspermanece no schema como legado (sem consumidor no runtime Clerk-only).
Histórico pré-Clerk (não implementado no runtime atual)
- Access token JWT Manypost (HS256), 15 min, claims
{sub, org, role}; cookie httpOnly. - Refresh opaco 30 dias com rotação e hash em
sessions. - Login social via env
GOOGLE_*/GITHUB_*na própria API.
- Formato
mp_live_<prefix8><secret32>; armazenada só o hash (sha256) + prefixo para lookup; exibida uma única vez. - Escopos:
posts:read,posts:write,channels:read,channels:write,media:write,analytics:read,webhooks:manage,mcp— múltiplas keys por org, revogação individual,last_used_at.
- manypost como authorization server no issuer
PUBLIC_URL(Next reescreve/.well-known/oauth-authorization-servere/oauth/*para a API): PRM no host MCP (/.well-known/oauth-protected-resource, RFC 9728); authorization code + PKCE S256 obrigatório; scopesmcp:read,mcp:write; tokensmpo_*opacos com hash no banco, expiração curta + refresh com rotação e detecção de reuso. - Endpoints de máquina do AS (
/oauth/authorize,/oauth/register,/oauth/token) são anônimos no front door Clerk (DCR/token não redirecionam para HTML de login). Consentimento continua autenticado. - Redirect URIs em loopback (
127.0.0.1/localhost/::1) seguem RFC 8252 §7.3 (porta ignorada; path/query exatos) para static, DCR e CIMD. - Clientes: estático público
manypost-mcp(Cursor + OpenCode loopback); CIMD (ex.: Claude Code); DCR em/oauth/register(Codex, VS Code, Inspector, Cursor paste-URL). Preferência: colar só a URL MCP. - Fallback universal:
Authorization: Bearer mp_live_…com escopomcp. - Tela de consentimento em
{PUBLIC_URL}/oauth/consent(Clerk): lista escopos, organização e hostname doredirect_uri(aviso em loopback); approve/deny devolvem o redirect comcode/error. - Dual-auth nas superfícies de máquina:
mp_live_(API key) oumpo_(OAuth); Clerk continua rejeitado no MCP/REST de máquina.
Cifrados at-rest (AES-256-GCM, SPEC_DATA §5); nunca expostos por nenhuma superfície; decrypt só no worker no momento do uso.
O serviço de API atende três hosts (roteamento por Host, mesmo processo, um deploy só):
| Host | Env | Serve |
|---|---|---|
| app (produto) | PUBLIC_URL |
/v1 interno (humano: Bearer Clerk ou cookie __session no SSE; autorização por papel), /public/approval, /uploads, /public/v1 (compat) e /mcp (compat) |
api.dominio |
API_PUBLIC_URL |
/v1 = esta API (mesmo sub-app de /public/v1, re-prefixado) + /openapi.json e /docs restritos a ela |
mcp.dominio |
MCP_PUBLIC_URL |
servidor MCP na raiz (§5), com /mcp como alias |
Por que host e não caminho: a superfície de máquina é Authorization: Bearer e nunca cookie, então não precisa ser same-origin com o web — e o rewrite do Next não é API gateway (bufferiza, tem timeout próprio, atrapalha o streaming do MCP e o pull de mídia grande). Os subdomínios são domínios custom do MESMO serviço, então não há CORS server-to-server, deploy extra nem divergência de código. PUBLIC_URL não se move: OAuth de canal, link de aprovação, URL pública de mídia e cookies dependem dela.
Sem as duas variáveis (self-host de um domínio só) nada muda: a API de máquina fica em {PUBLIC_URL}/public/v1 e o MCP em {PUBLIC_URL}/mcp. Os endereços efetivos são devolvidos por GET /v1/capabilities (endpoints.restBaseUrl/endpoints.mcpUrl) — é o que a UI mostra ao criar a chave.
Fronteira humano × máquina: API key mp_live_ é recusada com 403 no /v1 do host do app (a resposta aponta a superfície de máquina). Sem isso a chave contornaria escopos, gate de plano (public_api) e rate-limit por credencial, que só existem na superfície de máquina. CORS: qualquer origem, sem credentials (bearer, sem cookie), expondo mcp-session-id e os RateLimit-*.
Recursos (paridade com o Postiz + organização REST):
| Recurso | Endpoints | Escopo |
|---|---|---|
| Posts | GET/POST /posts, GET/PATCH/DELETE /posts/{groupId}, POST /posts/{groupId}/retry |
posts:* |
| Publications | GET /publications?state&from&to (status por canal) |
posts:read |
| Channels | GET /channels, GET /providers (catálogo + capacidades + settingsSchema como JSON Schema), DELETE /channels/{id} |
channels:* |
| Media | POST /media/upload (multipart, MIME real por magic bytes), POST /media/from-url (anti-SSRF), GET /media |
media:write |
| Analytics | GET /channels/{id}/analytics?range |
analytics:read |
| Webhooks | CRUD /webhooks (+ POST /webhooks/{id}/test) |
webhooks:manage |
| Aprovação por link | POST /posts/{groupId}/approval-link (cria/revoga; retorna URL única) |
posts:write |
GET /public/approval/{token}→ preview do grupo (conteúdo resolvido por canal, mídia, horário) — token opaco ≥ 128 bits, comparado por hash, single-purpose, com expiração; resposta nunca inclui dados da org além do necessário ao preview.POST /public/approval/{token}/approveePOST /public/approval/{token}/request-changes(com feedback) → transiciona o grupo (aprovado → elegível a agendar; ajustes → volta a rascunho com comentário), gravaaudit_log(actor_type=PUBLIC_LINK) e notifica a equipe.- Rate-limit agressivo por IP + token; sem enumeração (404 uniforme para token inválido/expirado); ação é idempotente (segunda chamada retorna o estado resolvido).
Regras:
- OpenAPI 3.1 gerado do zod é o contrato canônico (
/openapi.jsonpúblico); SDK TS gerado dele. - Erros: RFC 9457 (
application/problem+json) comcodeestável do domínio. - Rate limit por credencial: token bucket Redis, default 60 req/min + burst, headers
RateLimit-*; 429 comRetry-After. (Direção do Postiz — throttler Redis — com resposta padrão.) - Paginação por cursor (
?cursor=&limit=);Idempotency-Keyem todos os POST de mutação. - Anti-SSRF em qualquer fetch de URL de usuário (resolver DNS → bloquear IP privado — paridade com o dispatcher do Postiz).
- Versionamento no path (
/public/v1); breaking →/v2com sunset headers.
Eventos: post.published, post.failed, post.scheduled, channel.refresh_required, channel.disconnected. Entrega assinada X-manypost-Signature (HMAC-SHA256 com timestamp, tolerância 5 min), retries exponenciais (5 tentativas), endpoint de replay, filtro por canal — direção do Postiz (webhooks pós-publicação), formalizada.
- SDK oficial
@modelcontextprotocol/sdk, transporte Streamable HTTP no mesmo processo da api (desvio do Postiz: sem dependência de framework de agente; o MCP expõe use-cases direto). Endereço canônico = raiz do hostmcp.dominio(MCP_PUBLIC_URL) — é a URL que o usuário cola no cliente;/mcpresponde como alias, e no self-host de um domínio só ele fica em{PUBLIC_URL}/mcp. Navegação humana no host (GET comaccept: text/html) recebe uma página explicando como conectar, não um 401 cru. - Auth: API key com escopo
mcp(legado) ou OAuth §2 (mpo_+mcp:read/mcp:write); 401 sem credencial incluiWWW-Authenticatecomresource_metadata; discovery PRM no host MCP e AS emPUBLIC_URL. - Tools (paridade com o Postiz + política):
| Tool | Use-case | Escopo exigido |
|---|---|---|
list_channels |
listChannels | mcp:read |
list_posts / get_post |
queries de posts | mcp:read |
schedule_post |
schedulePost (mesma validação da API) | mcp:write |
update_post / cancel_post |
reagendar/cancelar | mcp:write |
upload_media_from_url |
ingest de mídia (anti-SSRF) | mcp:write |
get_channel_analytics |
analytics | mcp:read |
generate_content |
IA de criação (consome créditos) | mcp:write |
find_free_slot |
próximo horário livre | mcp:read |
- Resources:
manypost://channels,manypost://posts/{state}(leitura de contexto barata para o cliente MCP). - Políticas por tool: além do escopo, limites específicos (ex.:
schedule_postmáx 30/h por credencial) — mitiga agente em loop. - Toda chamada de tool grava
audit_logcomactor_type=MCP+ tool + argumentos resumidos (generalização doCreationMethoddo Postiz). - Respostas de tool são JSON estruturado estável (contrato versionado junto do OpenAPI).
OWNER (tudo), ADMIN (tudo menos billing/excluir org), MEMBER (criar/editar rascunhos, não aprova nem conecta canais) — matriz endpoint×papel na tabela de rotas, avaliada no mesmo middleware de escopos. Papéis finos/custom vivem no monorepo e são validados pelo PlanPolicy no SaaS ou liberados localmente em IS_SELF_HOSTED=true (SPEC_ARCHITECTURE §5).
- Refresh com rotação + detecção de reuso coberto por testes (reuso → família revogada).
- API key: criada → usada → revogada → 401; escopo insuficiente → 403 com
problem+json. - Fluxo MCP completo com um cliente real (Claude/Inspector): discovery → OAuth PKCE →
schedule_post→ post aparece no kanban com origem MCP. - Mesmo caso de uso inválido retorna o mesmo
codede erro via REST e via MCP (teste de paridade). openapi.jsonválido (spectral lint) e SDK TS gerado compila.- Rate limit e
Idempotency-Keyverificados por testes de integração (retry de POST não duplica post).
Specs irmãs: ARCHITECTURE · BACKEND · FRONTEND · DATA · QUEUE_PUBLISHING · INTEGRATIONS · AI · INFRA · ROADMAP
Navegação: Índice da documentação · STATUS · Decisões · Marca · README do projeto · Contribuir