Skip to content

Install Flow (OAuth-like)

TL;DR O PopinaFlow não implementa OAuth2 standard com redirect/code/exchange. Usamos um fluxo install-then-token mais simples: o admin do tenant aprova escopos diretamente no painel, e o backend emite um accessToken Bearer per-installation.

Por que não OAuth2 RFC-6749 puro

OAuth2 padrão presume que o usuário vive na sua App e é redirecionado ao provider para consentir. No caso do PopinaFlow o oposto é mais natural: o tenant já está logado no painel administrativo do PopinaFlow quando decide instalar sua App. O fluxo de redirect→callback adiciona atrito sem ganho de segurança nesse contexto.

Resultado: o consentimento acontece in-app (no /t/:slug/admin/marketplace) e o token cruza para a App publisher uma única vez, copy-once, sem callback HTTP.

A reintrodução de OAuth2 padrão pode acontecer em uma v2 da API se aparecer demanda real de Apps single-page baseadas em browser. Hoje, todas as integrações são server-to-server.

Atores e termos

TermoSignificado
AppDocumento marketplace_apps com slug, clientId, clientSecretHash, scopes[], webhookUrl. Visível para tenants quando status='published'.
InstallationDocumento marketplace_app_installations que liga tenant ↔ app com scopes[] (subset dos escopos da App), accessTokenHash, signingSecret, status.
accessTokenString hex de 128 chars (64 bytes random). Bearer token enviado em Authorization: Bearer <token> para /public/v1/*. Armazenado como bcrypt(rawToken, cost=12).
signingSecretString hex de 64 chars (32 bytes random) usada para assinar webhooks via HMAC-SHA256. Per-installation, mantido server-side, entregue ao publisher fora-de-banda no onboarding (veja webhooks.md).
clientId / clientSecretIdentidade do publisher. clientId é UUID v4 público; clientSecret é mostrado UMA vez na criação da App e armazenado como bcrypt. Não é usado em runtime hoje — reservado para v2.

Endpoints do install flow

Todas as rotas abaixo são tenant-scoped (vivem sob /t/:slug/admin/marketplace) e exigem que o usuário esteja autenticado no painel com role admin ou superadmin. Guard chain: JwtAuthGuard → TenantGuard → RolesGuard.

Listar apps publicadas

http
GET /t/:slug/admin/marketplace/apps
Authorization: Bearer <jwt-do-admin>

Retorna App[] com status='published'. O hash clientSecretHash é stripped da resposta.

Detalhe de uma App

http
GET /t/:slug/admin/marketplace/apps/:appSlug
Authorization: Bearer <jwt-do-admin>

404 para Apps em draft ou deprecated — só publicadas são visíveis.

Listar installations ativas do tenant

http
GET /t/:slug/admin/marketplace/installations
Authorization: Bearer <jwt-do-admin>

Retorna Installation[] com status='active', populadas com o documento App. Nunca retorna accessTokenHash nem signingSecret.

Instalar uma App

http
POST /t/:slug/admin/marketplace/apps/:appSlug/install
Authorization: Bearer <jwt-do-admin>
Content-Type: application/json

{
  "scopes": ["orders.read", "webhooks.subscribe"]
}

Validações server-side (em AppInstallService.installApp):

  1. App existe e está em status='published'. Caso contrário → 400.
  2. requestedScopes ⊆ App.scopes. Pedir escopo não declarado pela App → 400.
  3. Não existe instalação active desse {tenant, app}. Caso exista → 409 Conflict (precisa revogar antes de reinstalar).

Resposta de sucesso (201):

json
{
  "installation": {
    "_id": "65f1c8a2e4b0a8d1e5c3b9a7",
    "tenant": "65a0b1c2d3e4f5a6b7c8d9e0",
    "app": "65a0b1c2d3e4f5a6b7c8d9e1",
    "installedBy": "65a0b1c2d3e4f5a6b7c8d9e2",
    "scopes": ["orders.read", "webhooks.subscribe"],
    "status": "active",
    "createdAt": "2026-05-23T14:21:00.000Z"
  },
  "accessToken": "9b7f6e5d4c3b2a190f8e7d6c5b4a3928...128 hex chars total"
}

CRÍTICO — token mostrado UMA VEZ. O campo accessToken retorna em texto plano apenas nesta resposta. O servidor armazena somente bcrypt(rawToken, cost=12) em accessTokenHash. Não há endpoint para recuperá-lo depois. Frontend de consent modal deve apresentar UI copy-once e avisar o usuário antes de fechar.

A installation retornada não inclui accessTokenHash nem signingSecret — esses ficam restritos ao servidor.

Revogar uma installation

http
DELETE /t/:slug/admin/marketplace/installations/:id
Authorization: Bearer <jwt-do-admin>

Marca status='revoked' e gravando revokedAt. Idempotente — revogar algo já revogado é no-op (não rebumpa revokedAt). Após revogação, o accessToken correspondente para de validar imediatamente (no próximo bcrypt-compare ele falha porque a query do guard só considera installations ativas; veja S3 em public-api-v1.md).

A revogação também pára entregas de webhook: o eligibility check em WebhookDeliveryService.enqueueDelivery exige installation.status === 'active'.

Usando o accessToken para chamar a API pública

Em cada chamada para /public/v1/*:

http
GET /public/v1/orders HTTP/1.1
Host: popinaflow.alojaweb.online
Authorization: Bearer 9b7f6e5d4c3b2a190f8e7d6c5b4a3928...

Estado atual da auth (v1)

Importante (Maio 2026): o /public/v1/* hoje aceita o header legado x-api-key: <raw> (chave por-usuário, gap S3 em public-api-v1.md). A migração para Bearer per-installation é o caminho oficial — Apps novas devem ser construídas assumindo Bearer. O header x-api-key será descontinuado com 90 dias de aviso quando o ApiKeyGuard for substituído pelo guard de installation.

Resumo do que você precisa saber HOJE:

CenárioHeader
App nova (recomendado, contrato futuro)Authorization: Bearer <accessToken>
App legada já provisionada antes do marketplacex-api-key: <raw> + opcionalmente x-tenant-id: <objectId> (multi-tenant)

A semântica de tenant é resolvida automaticamente pelo token: cada installation pertence a exatamente um tenant.

Como o tenant te revoga

Há duas formas pelo lado do tenant:

  1. UI/t/:slug/admin/marketplace → aba "Instaladas" → botão "Revogar acesso". Usa useUndoToast (5s de janela de desfazer) e dispara DELETE na rota acima.
  2. API direta — admin/superadmin chama o DELETE por integração própria (raro, mas suportado para automações de gestão).

Sua App deve lidar com 401 Unauthorized em respostas a partir desse momento — significa que o tenant te baniu. Mostre uma mensagem clara no seu lado ("este restaurante revogou o acesso") e remova a installation do seu mapeamento interno.

Como o publisher te bane (kill-switch)

Em caso de violação de termos (vazamento de dados, abuso de escopo, etc.), a equipe PopinaFlow tem um kill-switch que define App.status='deprecated' ou marca todas as installations da App como revoked em batch. Comunicação aos tenants afetados acontece em <2h (canal: ticket de suporte + email).

Recomendação operacional para publishers: monitore 401 em sua telemetria — um spike concentrado provavelmente é kill-switch.

Boas práticas

  • Não armazene accessToken em texto claro. Use um vault (HashiCorp Vault, AWS Secrets Manager, Doppler, env-vars criptografadas). Trate o token como senha.
  • Não logue o accessToken. Truque comum: ao logar Authorization, redacte com Bearer ***.
  • Cache resultados onde fizer sentido. Endpoints de leitura (orders, menu) são throttle-limited; respeite 60s de cache para listagens grandes (menu/items).
  • Use TLS 1.2+ no seu webhookUrl. Endpoints HTTP plain são aceitos hoje mas serão rejeitados na revisão futura.
  • Implemente retry idempotente em chamadas POST — em caso de timeout do PopinaFlow, retentar pode duplicar — use chave de idempotência interna sempre que criar entidade (ex: reservation).

Lançado sob a licença MIT.