Pular para conteúdo

BFF — ficha piloto

Repositório: totvs-gateway-bff · Estado: Implementado · Responsável: time Gateway.

Versão documentada: 2f16f060de642dee03c61a0776caa11cc9167fbe (develop).

Esta ficha descreve o container BFF, seus contratos e suas fronteiras. As relações da plataforma estão no inventário de dependências; documentos e caches estão na modelagem de dados.

Responsabilidade

O BFF expõe APIs administrativas para o cliente administrativo externo, encaminha chamadas com contexto de tenant e adapta/agrega respostas. Também oferece fachadas públicas de token, JWKS, arquivos e documentação de catálogo. Core, Templates, Registry e serviços de políticas continuam responsáveis pelas respectivas regras e persistência.

O BFF não é o Broker: não orquestra a execução HTTP das APIs publicadas. Possui composições específicas, como enriquecimento de catálogo e obtenção de templates, além de relay SSE e transporte de arquivos. Fontes: integrações, CatalogForwardingService, configuração downstream.

O inventário completo das integrações do BFF identifica os 18 destinos HTTP e diferencia serviços de configuração, apoio compartilhado e domínios externos. As visões por domínio apresentam as chamadas verificadas nos forwarders.

Tecnologia e organização

Java 21, Quarkus 3.28.2, REST/Jackson, CDI, Bean Validation, SmallRye JWT/OpenAPI/Health/Fault Tolerance, Redis e OpenTelemetry. O POM referencia bibliotecas corporativas Accounts e Redis SNAPSHOT. DTOs, mappers e assemblers implementam as adaptações. O cliente compartilhado para forwarders usa java.net.http.HttpClient; Authenticator usa MicroProfile Rest Client.

Área Responsabilidade
integration/<destino>/resource HTTP, parâmetros, formatos, respostas e OpenAPI
integration/<destino>/service Encaminhamento, composição e adaptação específicas
integration/core/.../dto, mappers e assemblers Contratos e transformação das respostas administrativas
shared/http Cliente contextual, envelopes, binários, streams e relay SSE
integration/ipaas/security Filtro de acesso iPaaS
tenant/security Resolução do tenant e exceção pública independente
security Identidade, parser JWT, infraestrutura de autorização e propagação
shared/cache e integration/ratelimit/security Alias da resposta e limites de endpoints de identidade

Fontes: POM, pacote BFF, TenantAwareHttpClient.

Entrada / APIDomínio / processamentoSegurançaDados / cacheEventos / jobsSistema externo

Retângulos representam serviços ou grupos nomeados; cilindros, armazenamento; caixas com bordas duplas, mensageria; formas arredondadas, sistemas externos. As setas indicam chamadas ou transporte de dados, conforme o rótulo.

Componentes — C4 nível 3

Caminho de transporte

Como a entrada administrativa chega ao serviço de domínio? A visão destaca as três responsabilidades do BFF, sem misturar caches e streaming.

%%{init: {"theme":"base","themeVariables":{"fontFamily":"Arial, sans-serif","fontSize":"16px","lineColor":"#64748b","primaryTextColor":"#172b4d"},"flowchart":{"htmlLabels":true,"useMaxWidth":true,"nodeSpacing":28,"rankSpacing":38,"padding":14,"curve":"linear","subGraphTitleMargin":{"top":12,"bottom":16}},"sequence":{"useMaxWidth":true,"actorMargin":28,"width":145,"height":45,"diagramMarginX":32,"diagramMarginY":12,"messageMargin":24,"wrap":true,"mirrorActors":false},"themeCSS":".edgeLabel, .edgeLabel p {background-color:var(--diagram-surface)!important;color:var(--diagram-text)!important}\n.cluster rect {fill:var(--diagram-group)!important;stroke:var(--diagram-line)!important}\n.cluster-label span, .cluster-label text {color:var(--diagram-text)!important;fill:var(--diagram-text)!important}\n.flowchart-link, .actor-line {stroke:var(--diagram-line)!important}\nmarker path {fill:var(--diagram-line)!important;stroke:var(--diagram-line)!important}\n.actor {fill:var(--diagram-domain)!important;stroke:var(--diagram-domain-border)!important}\ntext.actor, .messageText, .loopText, .labelText {fill:var(--diagram-text)!important}\n.messageLine0, .messageLine1, .loopLine {stroke:var(--diagram-line)!important}\n.labelBox {fill:var(--diagram-surface)!important;stroke:var(--diagram-line)!important}\n.note {fill:var(--diagram-async)!important;stroke:var(--diagram-async-border)!important}\n.noteText {fill:var(--diagram-text)!important}\n.nodeLabel, .nodeLabel p, .nodeLabel span {color:var(--diagram-text)!important}\n.node .nodeLabel, .node .nodeLabel p {font-size:17px;font-weight:600;line-height:1.4;text-align:center}\n.nodeLabel small {display:inline-block;margin-top:4px;font-size:15px;font-weight:400;line-height:1.4;color:var(--diagram-description)!important}\ntext.actor, text.actor tspan {font-size:17px;font-weight:600}\n.node.entry rect, .node.entry polygon, .node.entry path, .node.entry circle {fill:var(--diagram-entry)!important;stroke:var(--diagram-entry-border)!important}\n.node.domain rect, .node.domain polygon, .node.domain path, .node.domain circle {fill:var(--diagram-domain)!important;stroke:var(--diagram-domain-border)!important}\n.node.security rect, .node.security polygon, .node.security path, .node.security circle {fill:var(--diagram-security)!important;stroke:var(--diagram-security-border)!important}\n.node.store rect, .node.store polygon, .node.store path, .node.store circle {fill:var(--diagram-store)!important;stroke:var(--diagram-store-border)!important}\n.node.async rect, .node.async polygon, .node.async path, .node.async circle {fill:var(--diagram-async)!important;stroke:var(--diagram-async-border)!important}\n.node.external rect, .node.external polygon, .node.external path, .node.external circle {fill:var(--diagram-external)!important;stroke:var(--diagram-external-border)!important}\n text.actor tspan, .loopText tspan, .labelText tspan, .messageText tspan, .noteText tspan {fill:var(--diagram-text)!important}\n\n"}}%%
flowchart TB
    client(["Cliente administrativo"]):::external
    subgraph boundary["BFF · componentes"]
      entry["JWT / Resources REST<br/><small>Entrada e contratos</small>"]:::entry
      forward["Forwarders e mappers<br/><small>Composição</small>"]:::domain
      http["TenantAwareHttpClient<br/><small>Transporte contextual</small>"]:::domain
      entry -->|Delega| forward
      forward -->|Prepara chamada| http
    end
    downstream(["Serviços de destino"]):::external
    client -->|HTTP| entry
    http -->|Tenant e tracing| downstream
    classDef entry stroke-width:1.5px;
    classDef domain stroke-width:1.5px;
    classDef external stroke-width:1.5px;

Resolução de tenant e limites

Quem consulta o tenant e onde os limites são contabilizados? O filtro resolve host somente quando a exceção pública aplicável exige esse contexto.

%%{init: {"theme":"base","themeVariables":{"fontFamily":"Arial, sans-serif","fontSize":"16px","lineColor":"#64748b","primaryTextColor":"#172b4d"},"flowchart":{"htmlLabels":true,"useMaxWidth":true,"nodeSpacing":28,"rankSpacing":38,"padding":14,"curve":"linear","subGraphTitleMargin":{"top":12,"bottom":16}},"sequence":{"useMaxWidth":true,"actorMargin":28,"width":145,"height":45,"diagramMarginX":32,"diagramMarginY":12,"messageMargin":24,"wrap":true,"mirrorActors":false},"themeCSS":".edgeLabel, .edgeLabel p {background-color:var(--diagram-surface)!important;color:var(--diagram-text)!important}\n.cluster rect {fill:var(--diagram-group)!important;stroke:var(--diagram-line)!important}\n.cluster-label span, .cluster-label text {color:var(--diagram-text)!important;fill:var(--diagram-text)!important}\n.flowchart-link, .actor-line {stroke:var(--diagram-line)!important}\nmarker path {fill:var(--diagram-line)!important;stroke:var(--diagram-line)!important}\n.actor {fill:var(--diagram-domain)!important;stroke:var(--diagram-domain-border)!important}\ntext.actor, .messageText, .loopText, .labelText {fill:var(--diagram-text)!important}\n.messageLine0, .messageLine1, .loopLine {stroke:var(--diagram-line)!important}\n.labelBox {fill:var(--diagram-surface)!important;stroke:var(--diagram-line)!important}\n.note {fill:var(--diagram-async)!important;stroke:var(--diagram-async-border)!important}\n.noteText {fill:var(--diagram-text)!important}\n.nodeLabel, .nodeLabel p, .nodeLabel span {color:var(--diagram-text)!important}\n.node .nodeLabel, .node .nodeLabel p {font-size:17px;font-weight:600;line-height:1.4;text-align:center}\n.nodeLabel small {display:inline-block;margin-top:4px;font-size:15px;font-weight:400;line-height:1.4;color:var(--diagram-description)!important}\ntext.actor, text.actor tspan {font-size:17px;font-weight:600}\n.node.entry rect, .node.entry polygon, .node.entry path, .node.entry circle {fill:var(--diagram-entry)!important;stroke:var(--diagram-entry-border)!important}\n.node.domain rect, .node.domain polygon, .node.domain path, .node.domain circle {fill:var(--diagram-domain)!important;stroke:var(--diagram-domain-border)!important}\n.node.security rect, .node.security polygon, .node.security path, .node.security circle {fill:var(--diagram-security)!important;stroke:var(--diagram-security-border)!important}\n.node.store rect, .node.store polygon, .node.store path, .node.store circle {fill:var(--diagram-store)!important;stroke:var(--diagram-store-border)!important}\n.node.async rect, .node.async polygon, .node.async path, .node.async circle {fill:var(--diagram-async)!important;stroke:var(--diagram-async-border)!important}\n.node.external rect, .node.external polygon, .node.external path, .node.external circle {fill:var(--diagram-external)!important;stroke:var(--diagram-external-border)!important}\n text.actor tspan, .loopText tspan, .labelText tspan, .messageText tspan, .noteText tspan {fill:var(--diagram-text)!important}\n\n"}}%%
flowchart TB
    jwt["IpaasJwtFilter"]:::security
    http["TenantAwareHttpClient"]:::domain
    quota["RateLimitService"]:::security
    tenant["TenantResolver"]:::security
    redis[("Redis<br/><small>Buckets de limite</small>")]:::store
    jwt -->|Host público| tenant
    http -->|Contexto| tenant
    quota -->|Owner| tenant
    quota -->|Lua| redis
    classDef domain stroke-width:1.5px;
    classDef security stroke-width:1.5px;
    classDef store stroke-width:1.5px;
Componente / relação Responsabilidade complementar
SmallRye JWT / IpaasJwtFilter → Resources Validar acesso antes do resource
Resources → RateLimitService Aplicar limite nos endpoints configurados
Resources → SseRelayService Encaminhar operações SSE
SseRelayService → TenantAwareHttpClient Transporte com Bearer e Last-Event-ID
DnsAliasResponseFilter → TenantResolver Obter tenant para enriquecimento
RateLimitService → Redis Contabilizar buckets com Lua (INCR / EXPIRE)

O desenho apresenta componentes lógicos dentro do mesmo processo. ResourceAuthorizationInterceptor existe, mas não é desenhado como etapa obrigatória: não há aplicações de @RequiresResource aos endpoints. Análise AA-001.

APIs e consumidores

Root HTTP /; porta declarada 8080. As constantes ApiPaths e ApiPathsV2 definem bases /gateway/api/v1 e /gateway/api/v2. A versão do endpoint BFF não determina a versão do downstream.

Família / path Métodos e propósito Destino principal
/gateway/api/v1/gateways e /{gatewayId} CRUD, habilitação, revisões, promoção/rollback Core
.../gateways/{gatewayId}/routes e /categories CRUD e ciclo de vida de rotas/categorias Core
.../gateways/{gatewayId}/wizard Leitura/atualização do wizard Core
/gateway/api/v2/gateways, rotas e /resources Contratos v2 e adaptações específicas Core / iPaaS / JSON Handler
/gateway/api/v1/authorizations Administração de autorizações Security
/gateway/api/v1/ip-policies, /rate-limits CRUD de políticas IP Policy / Rate Limit
/gateway/api/v1/gateway-templates e subresources Autoria, revisões, publicação e instanciação normal/de teste Template Service
POST .../gateways/{gatewayId}/template-upgrade Encaminhar solicitação de upgrade Template Service
GET /gateway/api/v1/operations/gateways/{gatewayId}/events SSE de progresso Template Service
POST /gateway/api/v1/operations/gateways/{gatewayId}/retry Solicitar nova tentativa Template Service
/gateway/api/v1/global-registry/catalog Catálogo e favoritos; documentação em subresource público Registry + Core + OpenAPI Doc
POST /gateway/api/v1/catalog/obtain/{catalogItemId} Obter definição e solicitar instanciação Registry + Template Service
/gateway/api/v1/tenant-aliases Administração de aliases Registry
/gateway/api/v1/resources, /json-handler Recursos/importação e transformação de schemas iPaaS / Importer / JSON Handler
/gateway/api/v1/monitoring Logs/métricas Monitoring
/gateway/api/v1/oauth2 Operações administrativas de identidade Auth Provider
/gateway/api/v1/object-storage/files Upload, leitura, metadados e publicação Object Storage
/gateway/api/v1/tunnels Administração e download de túnel Tunnel

Tabela de famílias, não especificação exaustiva. Métodos e parâmetros de cada subresource estão nos resources atuais. Referências específicas: OperationResource, TemplateUpgradeResource, CatalogResource.

Exceções públicas comprovadas

Endpoint Contrato de entrada e resposta Contexto
GET /.well-known/jwks.json JSON de certificados do Auth Provider PermitAll; tenant resolvido do host pelo filtro
POST /auth/token Form URL-encoded; client credentials; JSON de token; bucket token PermitAll; Basic ou campos do formulário, conforme contrato; tenant do host
GET /gateway/api/v1/object-storage/public/files/{fileId} Arquivo público PermitAll + TenantIndependent; leitura sem autenticação ou contexto de tenant
GET /gateway/api/v1/global-registry/catalog/{catalogItemId}/documentation JSON documental PermitAll + TenantIndependent; owner obtido no Registry
GET /static/catalog/{catalogItemId} HTML PermitAll + TenantIndependent
GET /static/catalog/templates/{gatewayTemplateId}?versionNo=... Resolve publicação e responde 303 com Location para a página do item PermitAll + TenantIndependent; erros 400/404/409 conforme contrato

Fontes: OAuth público, arquivos públicos, catálogo, páginas estáticas.

Identidade, autorização e tenant

IpaasJwtFilter ignora os prefixos de saúde/Swagger listados no código. Para recursos comuns, exige token presente e claim apps contendo ipaas (comparação sem distinção de caixa); retorna 401 sem token e 403 sem acesso ao app. A assinatura/issuer pertencem à configuração JWT/SmallRye e parser customizado; a checagem de apps não os substitui.

Para PermitAll, o filtro não exige esse JWT. Resolve tenant pelo primeiro label do host, exceto quando TenantIndependent é aplicado. TenantResolver.getTenantId() usa o tenant previamente resolvido ou claim companyId, rejeitando ausência/branco. O parâmetro x-tenant-id recebido do cliente não é a fonte dessa resolução no cliente compartilhado.

Fontes: IpaasJwtFilter, TenantResolver, parser, properties.

ResourceAuthorizationService implementa consulta ao Authenticator usando usuário/tenant, comparação de grupo/recurso e bypass para identidade técnica. RESOURCE_AUTHORIZATION_ENABLED tem default true, mas a flag não aplica a anotação aos endpoints. Não há evidência de RBAC geral concluído. O Bearer do usuário é propagado no AuthenticatorClient por provider explícito; não afirmar propagação automática em todos os clients.

Fontes: serviço de autorização, bypass, AuthenticatorClient, teste de isolamento Accounts/Authenticator.

Encaminhamento e transformações

TenantAwareHttpClient estabelece conexão com timeout de 30 s e deadline de 30 s para requests ordinários. Inclui x-tenant-id, JSON/Accept e propagação OTel. Headers extras são aplicados com setHeader, podendo substituir defaults em integrações específicas. O builder público não resolve tenant automaticamente; chamadas documentais podem acrescentar owner explicitamente.

getStringResponse lança DownstreamHttpException para status >= 400. exchange devolve status/body sem lançar por status HTTP. exchangeBytes e exchangeStream tratam corpo binário/stream e erros. Não há retry genérico implementado nesse cliente: presença de Fault Tolerance no POM não prova retry de todas as operações.

Fontes: cliente HTTP, mapper de erro.

findPaginated combina catálogo do Registry com estado de favoritos/instâncias consultado no Core. O enriquecimento adiciona favorite, acquired e acquiredGateways; remove o alias antigo aquired e o gatewayId singular. obtain deriva owner/template/versão da resposta do Registry e envia x-tenant-template-owner-id e catalog-id ao Template Service.

documentation resolve owner no Registry, depois consulta OpenAPI Doc com x-tenant-id desse owner. staticPage chama Web Pages por GET com body JSON {}; staticPageByTemplate resolve catálogo por versão e redireciona no mesmo origin. São contratos implementados, não recomendações genéricas de desenho de API.

Fontes: CatalogForwardingService, CatalogStaticResource, CatalogDocumentationTest.

Sequência interna ilustrada a partir do cliente real

Esta sequência descreve apenas a colaboração interna de forwarding com getStringResponse, após a validação de entrada; não é um fluxo E2E de negócio completo nem se aplica a todos os métodos de transporte.

Preparação do request

Como o cliente contextual adiciona tenant e tracing? O Resource já delegou a operação ao Forwarder.

%%{init: {"theme":"base","themeVariables":{"fontFamily":"Arial, sans-serif","fontSize":"16px","lineColor":"#64748b","primaryTextColor":"#172b4d"},"flowchart":{"htmlLabels":true,"useMaxWidth":true,"nodeSpacing":28,"rankSpacing":38,"padding":14,"curve":"linear","subGraphTitleMargin":{"top":12,"bottom":16}},"sequence":{"useMaxWidth":true,"actorMargin":28,"width":145,"height":45,"diagramMarginX":32,"diagramMarginY":12,"messageMargin":24,"wrap":true,"mirrorActors":false},"themeCSS":".edgeLabel, .edgeLabel p {background-color:var(--diagram-surface)!important;color:var(--diagram-text)!important}\n.cluster rect {fill:var(--diagram-group)!important;stroke:var(--diagram-line)!important}\n.cluster-label span, .cluster-label text {color:var(--diagram-text)!important;fill:var(--diagram-text)!important}\n.flowchart-link, .actor-line {stroke:var(--diagram-line)!important}\nmarker path {fill:var(--diagram-line)!important;stroke:var(--diagram-line)!important}\n.actor {fill:var(--diagram-domain)!important;stroke:var(--diagram-domain-border)!important}\ntext.actor, .messageText, .loopText, .labelText {fill:var(--diagram-text)!important}\n.messageLine0, .messageLine1, .loopLine {stroke:var(--diagram-line)!important}\n.labelBox {fill:var(--diagram-surface)!important;stroke:var(--diagram-line)!important}\n.note {fill:var(--diagram-async)!important;stroke:var(--diagram-async-border)!important}\n.noteText {fill:var(--diagram-text)!important}\n.nodeLabel, .nodeLabel p, .nodeLabel span {color:var(--diagram-text)!important}\n.node .nodeLabel, .node .nodeLabel p {font-size:17px;font-weight:600;line-height:1.4;text-align:center}\n.nodeLabel small {display:inline-block;margin-top:4px;font-size:15px;font-weight:400;line-height:1.4;color:var(--diagram-description)!important}\ntext.actor, text.actor tspan {font-size:17px;font-weight:600}\n.node.entry rect, .node.entry polygon, .node.entry path, .node.entry circle {fill:var(--diagram-entry)!important;stroke:var(--diagram-entry-border)!important}\n.node.domain rect, .node.domain polygon, .node.domain path, .node.domain circle {fill:var(--diagram-domain)!important;stroke:var(--diagram-domain-border)!important}\n.node.security rect, .node.security polygon, .node.security path, .node.security circle {fill:var(--diagram-security)!important;stroke:var(--diagram-security-border)!important}\n.node.store rect, .node.store polygon, .node.store path, .node.store circle {fill:var(--diagram-store)!important;stroke:var(--diagram-store-border)!important}\n.node.async rect, .node.async polygon, .node.async path, .node.async circle {fill:var(--diagram-async)!important;stroke:var(--diagram-async-border)!important}\n.node.external rect, .node.external polygon, .node.external path, .node.external circle {fill:var(--diagram-external)!important;stroke:var(--diagram-external-border)!important}\n text.actor tspan, .loopText tspan, .labelText tspan, .messageText tspan, .noteText tspan {fill:var(--diagram-text)!important}\n\n"}}%%
sequenceDiagram
    participant F as Forwarder
    participant H as Cliente HTTP
    participant T as TenantResolver
    F->>H: buildGet / buildPost
    H->>T: getTenantId()
    T-->>H: companyId / contexto
    H-->>F: Request contextual

Transporte e resultado

Como o status downstream retorna à camada de aplicação? O mesmo cliente executa o request preparado.

%%{init: {"theme":"base","themeVariables":{"fontFamily":"Arial, sans-serif","fontSize":"16px","lineColor":"#64748b","primaryTextColor":"#172b4d"},"flowchart":{"htmlLabels":true,"useMaxWidth":true,"nodeSpacing":28,"rankSpacing":38,"padding":14,"curve":"linear","subGraphTitleMargin":{"top":12,"bottom":16}},"sequence":{"useMaxWidth":true,"actorMargin":28,"width":145,"height":45,"diagramMarginX":32,"diagramMarginY":12,"messageMargin":24,"wrap":true,"mirrorActors":false},"themeCSS":".edgeLabel, .edgeLabel p {background-color:var(--diagram-surface)!important;color:var(--diagram-text)!important}\n.cluster rect {fill:var(--diagram-group)!important;stroke:var(--diagram-line)!important}\n.cluster-label span, .cluster-label text {color:var(--diagram-text)!important;fill:var(--diagram-text)!important}\n.flowchart-link, .actor-line {stroke:var(--diagram-line)!important}\nmarker path {fill:var(--diagram-line)!important;stroke:var(--diagram-line)!important}\n.actor {fill:var(--diagram-domain)!important;stroke:var(--diagram-domain-border)!important}\ntext.actor, .messageText, .loopText, .labelText {fill:var(--diagram-text)!important}\n.messageLine0, .messageLine1, .loopLine {stroke:var(--diagram-line)!important}\n.labelBox {fill:var(--diagram-surface)!important;stroke:var(--diagram-line)!important}\n.note {fill:var(--diagram-async)!important;stroke:var(--diagram-async-border)!important}\n.noteText {fill:var(--diagram-text)!important}\n.nodeLabel, .nodeLabel p, .nodeLabel span {color:var(--diagram-text)!important}\n.node .nodeLabel, .node .nodeLabel p {font-size:17px;font-weight:600;line-height:1.4;text-align:center}\n.nodeLabel small {display:inline-block;margin-top:4px;font-size:15px;font-weight:400;line-height:1.4;color:var(--diagram-description)!important}\ntext.actor, text.actor tspan {font-size:17px;font-weight:600}\n.node.entry rect, .node.entry polygon, .node.entry path, .node.entry circle {fill:var(--diagram-entry)!important;stroke:var(--diagram-entry-border)!important}\n.node.domain rect, .node.domain polygon, .node.domain path, .node.domain circle {fill:var(--diagram-domain)!important;stroke:var(--diagram-domain-border)!important}\n.node.security rect, .node.security polygon, .node.security path, .node.security circle {fill:var(--diagram-security)!important;stroke:var(--diagram-security-border)!important}\n.node.store rect, .node.store polygon, .node.store path, .node.store circle {fill:var(--diagram-store)!important;stroke:var(--diagram-store-border)!important}\n.node.async rect, .node.async polygon, .node.async path, .node.async circle {fill:var(--diagram-async)!important;stroke:var(--diagram-async-border)!important}\n.node.external rect, .node.external polygon, .node.external path, .node.external circle {fill:var(--diagram-external)!important;stroke:var(--diagram-external-border)!important}\n text.actor tspan, .loopText tspan, .labelText tspan, .messageText tspan, .noteText tspan {fill:var(--diagram-text)!important}\n\n"}}%%
sequenceDiagram
    participant F as Forwarder
    participant H as Cliente HTTP
    participant D as Serviço destino
    F->>H: getStringResponse
    H->>D: HTTP
    D-->>H: status + body
    alt status >= 400
        H-->>F: Erro downstream
    else sucesso
        H-->>F: body
    end

Em caso de erro, DownstreamHttpException atravessa o Forwarder até o Resource e o ExceptionMapper conserva status/body. No sucesso, o Forwarder adapta o resultado quando aplicável e o devolve ao Resource. Cliente HTTP nos dois desenhos corresponde a TenantAwareHttpClient.

SSE, arquivos e efeitos assíncronos

O BFF expõe SSE de progresso de instanciação/upgrade e relay para o Template Service. buildStreamGet captura tenant, Bearer e Last-Event-ID, sem deadline ordinário de 30 s. SseRelayService usa publisher HTTP e controle de demanda conforme a escrita; cancela subscription/exchange quando o cliente encerra. Resposta 200 sem text/event-stream vira 502. O relay transmite eventos; não garante replay, conclusão de operação ou idempotência do produtor.

O BFF não persiste uma operação nem consome RabbitMQ para esse relay. A aceitação/retry de operação e sua conclusão pertencem ao Template Service. Binários e multipart usam caminhos de transporte próprios; não descrever tudo como serialização JSON.

Fontes: SseRelayService, OperationStreamService, Object Storage integration, OperationStreamTest.

Estado local e cache

Não foi encontrado ownership de documentos MongoDB no BFF. O estado relevante identificado é contexto por request e Redis para mecanismos auxiliares.

Uso Implementação Política observada no código
Header dns-alias Lookup tenant-alias:<tenantId> Timeout 30 ms; erro/miss omite enriquecimento; TTL de escrita não determinado por esse leitor
Limite por bucket rate-limit:<tenantId>:<bucket> Lua INCR e EXPIRE na primeira ocorrência; excesso gera 429
Redis indisponível no limite BFF RedisRateLimitCache retorna contagem 0 Fail-open explícito; não confundir com quota do Checker

Fontes: resolver de alias, filtro de resposta, RateLimitService, Lua/fallback.

Erros e garantias

Situação Resultado comprovado Fonte
Token ausente no filtro 401, JSON error/message IpaasJwtFilter
Sem apps=ipaas 403, JSON error/message IpaasJwtFilter
companyId ausente 403 pelo mapper MissingJwtClaimExceptionMapper
Host público inválido 400 pelo mapper TenantResolutionExceptionMapper
Bucket excedido 429, TOO_MANY_REQUESTS RateLimitExceededExceptionMapper
DownstreamHttpException Preserva status/body, Content-Type JSON Mapper
IOException/interrupção no cliente ordinário IllegalStateException; não há normalização genérica explícita para 502 nesse cliente TenantAwareHttpClient
SSE 200 com tipo inválido 502 SseRelayService

Não há envelope único de erro comprovado para todas as famílias. Anotações OpenAPI precisam ser comparadas com mappers e transporte; não concluir que todo erro de rede é 502. Retry/idempotência de operações compostas dependem dos serviços chamados; não há transação distribuída BFF entre Core, Registry e Templates.

Configuração e deployment

Ambientes separados: defaults abaixo vêm do código e não comprovam valores efetivos de DEV, STAGING ou PROD. O deployment descrito a seguir pertence exclusivamente ao ambiente local; a documentação da infraestrutura e dos deployments corporativos é mantida pelo TOTVS Apps.

Configuração Finalidade / default relevante
GATEWAY_*_BASE_URL e CONNECTOR_* URLs downstream; defaults incluem localhost/ambientes de desenvolvimento; conferir overrides
ACCOUNTS_JWKS_URL JWKS JWT; fallback compatível ACCOUNTS_JKWS_URL
ACCOUNTS_JWT_ISSUER Default literal *.fluig.io; não é padrão wildcard de matching
GATEWAY_JWT_JOSE4J_RESPONSE_BODY_SIZE_LIMIT Default 10485760 caracteres para resposta JWKS
RESOURCE_AUTHORIZATION_ENABLED Default true para mecanismo quando acionado
AUTHENTICATOR_URL Base do client de permissões
BFF_RATE_LIMIT_* Buckets token/emit-token/test-token/test-discovery; defaults de limite 5 e TTL 60 s nas properties
QUARKUS_OTEL_ENABLED, traces e metrics Defaults false nas properties; deployment pode sobrescrever
OTEL_EXPORTER_OTLP_ENDPOINT Exportador OTLP gRPC, conforme ambiente

Nenhuma credencial foi reproduzida. Propriedades proxy permitem X-Forwarded-* e host encaminhado; fronteira confiável depende do deployment. Fontes: application.properties, DownstreamServiceConfig.

Deployment declarado no ambiente local

O chart local usa Deployment/Service na porta 8080, ConfigMap/Secret, imagem default bff:develop e valores de réplica 1. Probes: /gateway/q/health/started, /ready, /live. Recursos default: requests 250m/256Mi, limits 1 CPU/1Gi. Esses são valores declarados, não consumo medido. O Ingress contém rewrite/CORS locais; não assumir comportamento produtivo equivalente. Redis health está desabilitado nas properties, logo readiness não comprova saúde Redis.

Fontes: deploy.yaml, values, Ingress, ConfigMap. O DNS de Monitoring e a falta de alguns overrides documentais estão registrados em AA-006.

Observabilidade e testes

O cliente injeta contexto OTel; há logging de relay SSE, falhas de cache e autorização. Exportação, retenção e dashboards dependem da configuração de observabilidade do ambiente. OpenAPI está configurado em /swagger, UI em /swagger-ui e endpoints não funcionais em /gateway/q.

Teste existente Evidência de cenário
IpaasJwtFilterTest Exceções públicas e filtragem
TenantResolverTest Claim e resolução de host
AuthClientIsolationTest Accounts mantém Basic; Authenticator recebe Bearer do usuário
CatalogDocumentationTest Owner documental e preservação de status/body
CatalogObtainTest Composição de obtenção
OperationStreamTest Transporte SSE
RedisRateLimitCacheTest Redis e fallback
GatewayResponseAssemblerTest Adaptação da resposta administrativa

Testes foram consultados, não executados nesta tarefa documental. Para validação de código em tarefa própria, usar Java 21 e Maven Wrapper no repositório BFF, conforme seu AGENTS.md; não há build único do workspace.

Pendências de confirmação

Execução e imagem atuais; proteção efetiva dos endpoints internos; aplicação completa de autorização por recurso; limites operacionais de SSE/caches; compatibilidade dos URLs do chart e clientes; autorização geral cross-tenant. Os registros de análise acompanham essas questões sem alterar a implementação.