Repository navigation
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I'll start by reading the mandatory project memory, then the plan file, before any analysis.
Memória lida. Agora vou ler as sub-memórias de F7 e o plano de detalhe da Task 7.3.
Memórias lidas. Vou agora listar os planos de detalhe e consultar o graphify.
Graphify disponível. Vou consultar o grafo sobre reviews.
Graphify confirma os módulos. Vou agora inventariar os ficheiros reais de F7 (7.1 e 7.2).
Ficheiros presentes. Vou verificar a Task 7.1 em detalhe (endpoint interno + filtro de token).
Task 7.1 core confirmada. Vou verificar repositório, security config, YAML e messages.
Plan mode está ativo — o meu trabalho aqui é read-only na mesma, por isso continuo a auditoria sequencialmente (sem sub-agents, conforme pediste). Vou verificar a Camada 1 (gateway) e a Task 7.2.
Plan mode saiu. Continuo a auditoria. Vou verificar as predicates reais do gateway (Camada 1).
Backend verificado. Agora a Task 7.4 (frontend).
Frontend confirmado. Agora a validação da documentação (T3, API.md, etc.).
Vou obter evidência dura de que tudo compila hoje (
test-compile, não corrida de testes — essa é tua).Auditoria concluída. Aqui está o resultado.
Validação F7 —
.claude/plans/2026-07-18-f7-reviews.mdMétodo: memória lida primeiro (índice + 3 sub-ficheiros F7), graphify para localizar os nós, depois leitura ficheiro-a-ficheiro. Só PowerShell, sem sub-agents, tudo sequencial. Nada foi alterado.
1. Implementação — COMPLETA (código)
6. Veredito e próxima tarefa
Não posso chamar-lhe "done". O código está 100% implementado e compila; a verificação por browser está registada em memória. Falta o closeout — que o próprio plano define como condição de fecho.
Próxima tarefa recomendada — “F7 Closeout: docs + Helm”, por esta ordem:
docs/engineering/service-to-service-auth.md(T3):X-Internal-Token, chave Vault,InternalTokenFilter,OrderClientConfig, checklist "como adicionar um novo/internalseguro" — e corrigir aí a afirmação da Camada 1 para o que o gateway realmente faz..claude/docs/api/API.md(5 endpoints + o interno, com os códigos 201/403/404/409/503).INTERNAL_SERVICE_TOKENaos dois ramos dosecret.yaml(+values.yaml), espelhando oJWT_SECRET.mvn verify -pl order-service,product-service -Dapi.version=1.44enpm run build+npm run lint.Queres que execute o closeout (passos 1–4)? São aditivos, sem apagar nada, e nenhum toca em código de runtime exceto o Helm.
I'll start by reading the mandatory project memory, then the plan file, before any analysis.Memória lida. Agora vou ler as sub-memórias de F7 e o plano de detalhe da Task 7.3.
Memórias lidas. Vou agora listar os planos de detalhe e consultar o graphify.
Graphify disponível. Vou consultar o grafo sobre reviews.
Graphify confirma os módulos. Vou agora inventariar os ficheiros reais de F7 (7.1 e 7.2).
Ficheiros presentes. Vou verificar a Task 7.1 em detalhe (endpoint interno + filtro de token).
Task 7.1 core confirmada. Vou verificar repositório, security config, YAML e messages.
Plan mode está ativo — o meu trabalho aqui é read-only na mesma, por isso continuo a auditoria sequencialmente (sem sub-agents, conforme pediste). Vou verificar a Camada 1 (gateway) e a Task 7.2.
Plan mode saiu. Continuo a auditoria. Vou verificar as predicates reais do gateway (Camada 1).
Backend verificado. Agora a Task 7.4 (frontend).
Frontend confirmado. Agora a validação da documentação (T3, API.md, etc.).
Vou obter evidência dura de que tudo compila hoje (
test-compile, não corrida de testes — essa é tua).Auditoria concluída. Aqui está o resultado.
Validação F7 —
.claude/plans/2026-07-18-f7-reviews.mdMétodo: memória lida primeiro (índice + 3 sub-ficheiros F7), graphify para localizar os nós, depois leitura ficheiro-a-ficheiro. Só PowerShell, sem sub-agents, tudo sequencial. Nada foi alterado.
1. Implementação — COMPLETA (código)
InternalPurchaseController,InternalTokenFilter, query exists,OrderSecurityConfig, yml tokenCONFIRMED/SHIPPED/DELIVERED; comparação constant-time (MessageDigest.isEqual); fail-closed com token vazio;shouldNotFilterlimita a/internal;addFilterBeforeantes do JWTreview/, V13, security, 5 contratospomopenfeign + resilience4j;@EnableFeignClients; V13 comUNIQUE(product_id, customer_id)sem tenant (T1 cumprido); timeouts 2000/2000 e CB 10/50%/15s exatamente como o plano; sem fallback permissivorecomputeRatingCounters+lockProductForRatingRecomputeFOR NO KEY UPDATE;evictProductDetailviaProductCacheKeys;product-listintocadoreviews.api.ts, detalhe,ProductCard, i18n EN/FR/PT/ESStarRating/ReviewList/ReviewForm/ReviewsSection/AdminReviewsPage, rota e nav admin; i18n nos 4 idiomasEvidência de compilação (corrida agora):
mvn -B test-compile -pl order-service,product-service→ BUILD SUCCESS (68+40 e 46 ficheiros).Extensão além do plano (aprovada por ti, registada em [2026-07-19-f7-task-7.4-reviews-ui.md](.claude/plans/2026-07-19-f7-task-7.4-reviews-ui.md)): 7.4a acrescentou 2 endpoints que o plano F7 não previa —
GET /{id}/reviews/me(elegibilidade, 200 comVERIFICATION_UNAVAILABLE, deliberadamente não fail-closed) eGET /products/reviews/admin(feed de moderação), ambos com matcher.authenticated()antes dopermitAlldeGET /products/**— verifiquei em [ProductSecurityConfig.java:60-62](product-service/src/main/java/code/with/vanilson/productservice/config/ProductSecurityConfig.java:60).2. Testes — escritos nas 4 camadas, MAS sem prova de execução
InternalTokenFilterTest×6ReviewServiceTest×24InternalPurchaseControllerTest×3ReviewControllerTest×18InternalPurchaseVerificationIntegrationTest×6ReviewIntegrationTest×15,CatalogRatingIntegrationTest×9,ReviewConcurrencyIntegrationTest×4internal_purchase_verification.feature×4product_reviews.feature×11,catalog_rating.feature×6C4 (concorrência PG real) presente; 503 fail-closed coberto em unit+slice+integration+BDD; zero H2. Features registadas no
CucumberTestRunnerdo product-service; o runner do order-service seleciona o diretório inteiro.target/dos dois módulos só tinhaclasses— zerosurefire-reports/failsafe-reports. Não consigo determinar se correstemvn verifynoutra altura e limpaste; o que é facto é que não existe evidência de execução neste workspace. Por regra, quem corre és tu.Nunca provado (assumido no próprio plano): 503 fail-closed real (exige parar o order-service), concorrência fora do IntegrationTest, e
/products/mine(5.º site doProductMapper).3. Documentação — INCOMPLETA (3 falhas + 2 desvios)
docs/engineering/service-to-service-auth.mdNÃO EXISTE. Era entregável explícito do plano (T3: "SIM — entregável do closeout"). Confirmei: zero ocorrências deX-Internal-Tokenem qualquer.mddo projeto.averageRating/reviewCount([API.md:1938](.claude/docs/api/API.md:1938)). Zero ocorrências de/reviewsouinternal/purchasesem toda a documentação — faltam 5 endpoints públicos + 1 interno.- [ ]apesar de implementadas e browser-verificadas; só 7.3 está- [x]. O plano de fase também só tem bloco de estado para a 7.3. O gap-analysis ainda diz "Fases 5–7 ... remain open".DELETE /api/v1/products/reviews/{id}em vez de/api/v1/reviews/{id}— para reutilizar o prefixo já roteado pelo gateway./internalfica "FORA das predicates do gateway... inalcançável de fora". Na realidade [gateway-service.yml:161](config-service/src/main/resources/configurations/gateway-service.yml:161) temPath=/api/v1/orders/**, que casa com/internal; o que existe é ausência depublic-paths→ anónimo leva 401, mas qualquer JWT válido chega ao endpoint e é oInternalTokenFilterque o barra. O comentário no código descreve isto corretamente; é o plano que sobre-afirma.4. 🔴 Achado novo — risco de deploy (não estava em nenhuma memória)
INTERNAL_SERVICE_TOKENnão existe no Helm chart. [secret.yaml](helm/ecommerce/templates/secret.yaml) enumeraJWT_SECRET,KAFKA_PASSWORD,REDIS_PASSWORD,VAULT_TOKEN,EUREKA_PASSWORD— nos dois ramos (ExternalSecret e Secret simples) — e o deployment consome porenvFrom: secretRef. Como os dois YAMLs de config têminternal-token: ${INTERNAL_SERVICE_TOKEN}sem default, num deploy Helm/k8s o placeholder fica por resolver.Consequência esperada: falha no arranque de order-service e product-service (resolução de placeholder aninhado), exatamente o mesmo padrão do
JWT_SECRET— que está wired. Não verifiquei a correr, por isso digo-o como alta-confiança, não como provado. Docker compose (dev,.prod,.scaleporextends) está correto.5. Tradeoffs, prós e contras do que foi entregue
GET /productsnão regrideFOR NO KEY UPDATE(oFOR UPDATEfazia deadlock N-way com o FK lock)X-Internal-TokenpermitAlldo customer-service/products/6. Veredito e próxima tarefa
Não posso chamar-lhe "done". O código está 100% implementado e compila; a verificação por browser está registada em memória. Falta o closeout — que o próprio plano define como condição de fecho.
Próxima tarefa recomendada — “F7 Closeout: docs + Helm”, por esta ordem:
docs/engineering/service-to-service-auth.md(T3):X-Internal-Token, chave Vault,InternalTokenFilter,OrderClientConfig, checklist "como adicionar um novo/internalseguro" — e corrigir aí a afirmação da Camada 1 para o que o gateway realmente faz..claude/docs/api/API.md(5 endpoints + o interno, com os códigos 201/403/404/409/503).INTERNAL_SERVICE_TOKENaos dois ramos dosecret.yaml(+values.yaml), espelhando oJWT_SECRET.mvn verify -pl order-service,product-service -Dapi.version=1.44enpm run build+npm run lint.Queres que execute o closeout (passos 1–4)? São aditivos, sem apagar nada, e nenhum toca em código de runtime exceto o Helm.
Executo o closeout. Uma tarefa de cada vez, com nota no fim de cada uma.
Task 1/4 —
docs/engineering/service-to-service-auth.md. Primeiro leio o estilo dos docs existentes e os últimos detalhes de contrato.Vou inverter a ordem: faço primeiro o Helm (o antigo passo 4) e só depois escrevo o doc — a regra mandatória diz que os docs seguem código já verificado, e escrever o doc antes do fix criaria drift no próprio dia.
Task 1/4 — Helm:
INTERNAL_SERVICE_TOKEN. Leio os ficheiros reais primeiro.Confirmado:
values-production.yamlnão tem blocosecrets(usa ExternalSecret). Faço as 4 edições aditivas, espelhando exatamente oJWT_SECRET.Verifico que o
envFromconsome mesmo este Secret e, se ohelmexistir, renderizo o template.Helm verificado (
helm lintverde, ambos os ramos renderizam).Nota T1: o ramo ExternalSecret passa a exigir a chave Vault
secret/ecommerce/shared → internal_service_token— pré-requisito operacional que vou documentar já a seguir.Task 2/4 —
docs/engineering/service-to-service-auth.md. Leio a feature BDD para descrever o comportamento sem inventar.Agora escrevo o documento T3.
Corrijo um lapso que me escapou no ficheiro (uma palavra em cirílico no item 8 da checklist).
Nota T2: doc criado com todas as referências file:line verificadas; corrigi a afirmação sobre a Camada 1 (o gateway rota
/internal; quem barra é o filtro).Task 3/4 — API.md. Leio a estrutura real do documento para inserir na secção certa.
Tenho os contratos reais. Insiro agora a secção Reviews no API.md.
Agora o endpoint interno, na secção Order Service.
Nota T3: API.md ganhou 6 endpoints (5 públicos + 1 interno), com os contratos lidos dos records reais.
Task 4/4 — planos e gap-analysis. Leio as linhas exatas antes de editar.
Agora o gap-analysis (banner desatualizado + a afirmação errada em D2).
Falta o plano de fase — o próprio "closeout obrigatório" da 7.4 exigia atualizá-lo.
Falta o closeout de memória (regra mandatória). Crio a memória da lição nova e atualizo o índice.
Agora atualizo o índice de memória e a linha desatualizada do épico.
Verificação final: consistência dos docs,
git statuse atualização do grafo.git statusmostra só 3 ficheiros — sinal de quedocs/e.claude/estão ignorados. Confirmo.Facto relevante:
docs/e.claude/estão em.gitignore(linhas 49 e 53, pré-existente) — as alterações de documentação ficam fora do controlo de versões. Atualizo o grafo e confirmo os ficheiros em disco.Closeout concluído. Relatório final.
Relatório — F7 Closeout
1. Implementation summary
Executei os 4 passos, com uma alteração de ordem que assinalei antes de começar: fiz o Helm primeiro, porque a regra mandatória de consistência diz que os docs seguem código já verificado — escrever o documento S2S antes do fix criaria drift no próprio dia.
T1 — Helm (
INTERNAL_SERVICE_TOKEN). A env var existia nos dois composes mas não no chart. Acrescentei-a aos dois ramos dosecret.yaml(ExternalSecret →secret/ecommerce/sharedpropertyinternal_service_token; Secret simples com default de CI) e àsvalues. Verificado comhelm template --show-only templates/secret.yamlnos dois modos ehelm lint→ 0 charts failed.T2 —
docs/engineering/service-to-service-auth.md(entregável T3 do plano, não existia). Criado com as 3 camadas, tabela das 4 vias de configuração, modos de falha (401→503), checklist de "como adicionar um novo/internalseguro", testes e limitações. Todas as referênciasfile:lineforam lidas, não citadas de memória.T3 —
.claude/docs/api/API.md. Documentados os 5 endpoints públicos de review + o endpoint interno, com os contratos extraídos dosrecordreais (ReviewRequest/Response,ReviewEligibilityResponse,AdminReviewResponse), incluindo a assimetria deliberada POST=503 vs elegibilidade=200 e a política de cache A1.T4 — Planos e gap-analysis. Checkboxes 7.1/7.2/7.4 fechadas, banner de estado da Fase 7, e os três desvios plano↔código passados a escrito em vez de ficarem como drift silencioso — sobretudo a "Camada 1", que o plano e o D2 do gap-analysis descreviam como "não roteado pelo gateway" quando na realidade
Path=/api/v1/orders/**casa com/internal; só o anónimo é travado, e quem protege é oInternalTokenFilter.Closeout de memória (regra mandatória): 2 memórias novas + índice + a entrada do épico atualizada.
2. Files changed
Código/infra (3, tracked em git):
helm/ecommerce/templates/secret.yaml—INTERNAL_SERVICE_TOKENnos dois ramoshelm/ecommerce/values.yaml—internalServiceToken: ""helm/ecommerce/values-staging.yaml— valor de CIDocumentação (5, não tracked — ver riscos):
docs/engineering/service-to-service-auth.md(novo, 12.3 KB).claude/docs/api/API.md— secção Reviews + Internal Purchase Verificationdocs/superpowers/plans/2026-07-11-marketplace-role-capabilities.md— Fase 7 reconciliadadocs/engineering/marketplace-capabilities-gap-analysis.md— banner + correção do D2.claude/plans/2026-07-18-f7-reviews.md— estado final da faseMemória (4):
project_f7_closeout_docs_2026_07_26.md(novo),reference_new_env_var_needs_helm_secret.md(novo),MEMORY.md,project_marketplace_role_capabilities_spec.md.Nada foi apagado; todas as edições são aditivas. Grafo atualizado (
graphify update .→ 7661 nós).3. Bugs reproduzidos antes do fix?
Não, e quero ser explícito nisto. O único defeito real desta sessão foi o
INTERNAL_SERVICE_TOKENausente do Helm, e não o reproduzi em runtime — não há cluster de pé e não corro Docker.O que tenho é prova estática, não empírica:
grep INTERNAL_SERVICE_TOKENemhelm/→ zero ocorrências antes do fix; leitura integral dosecret.yamla confirmar que enumeraJWT_SECRET,KAFKA_PASSWORD,REDIS_PASSWORD,VAULT_TOKEN,EUREKA_PASSWORDe mais nada; edeployment.yaml:60a consumir porenvFrom: secretRef. A consequência que descrevo — falha de arranque do contexto Spring por placeholder aninhado não resolvido — é raciocínio a partir do código, não observação. Fica por confirmar num deploy real.O defeito da query key (string vs number) foi encontrado e corrigido numa sessão anterior; não o revalidei aqui.
4. Remaining risks
target/dos dois módulos só tinhaclasses— semsurefire-reportsnemfailsafe-reports. Corri apenasmvn test-compile(BUILD SUCCESS). Não é determinável se correram noutra altura e o target foi limpo. Faltam:secret/ecommerce/shared → internal_service_token. Um Vault provisionado antes de hoje não tem essa property, e o External Secrets Operator não materializa o Secret até alguém a criar — ou seja, o meu fix desbloqueia o caminho k8s mas só fica completo depois desse passo operacional.docs/e.claude/estão em.gitignore(linhas 49 e 53, pré-existente). Toda a documentação que acabei de escrever — incluindo o doc S2S — vive fora do controlo de versões. Não alterei isto porque não é âmbito do closeout, mas é o risco estrutural mais silencioso aqui: o doc que descreve o padrão de segurança S2S não é partilhável nem versionado.ReviewConcurrencyIntegrationTesta prova). O 5.º site doProductMapper(/products/mine) também nunca foi exercitado por UI./api/v1/orders/**do gateway; deliberadamente não o fiz, por ser mudança de comportamento fora do âmbito aprovado.Aceite: vou resolver os dois riscos — A) bloquear
/internalna borda (Camada 1 real) e B) rotação + identidade do chamador + extração para starter partilhado. Uma tarefa de cada vez.Primeiro, a regra path-scoped obrigatória do gateway e o código real.
Padrão do gateway compreendido. Antes de escrever, confirmo que bloquear
/internalna borda não parte nada (SPA e outros serviços).SPA nunca chama
/internal✅. Mas antes de bloquear tenho de confirmar que os chamadores S2S vão diretos ao serviço e não através do gateway — se algum passasse pelo gateway, bloquear partia o registo.Confirmado: ambos os chamadores S2S vão diretos ao host do serviço (
customer-service:8090,order-service:8083), nunca pelo gateway. Bloquear na borda é seguro.Task A/B — filtro de bloqueio no gateway. Leio o estilo de teste do módulo.
Tenho o padrão exato. Escrevo o filtro.
Agora o teste unitário (o módulo gateway só tem esta camada — ver nota no fim).
Agora torno a configuração explícita no
gateway-service.yml(a par depublic-paths).Nota A: filtro
InternalPathBlockFilter(404 sem leak, ordem +5, antes do JWT), chave de mensagem, config explícita e teste com 5 grupos.test-compileverde.Task B/B — hardening S2S. Leio o
tenant-context(é lá que vive oJwtAuthenticationFilterpartilhado — o sítio natural para o starter).Estrutura clara. Escrevo o núcleo partilhado no
tenant-context— propriedades + autenticador (a lógica pura e testável).Agora o interceptor Feign partilhado (lado do chamador) e a auto-configuração.
Teste do núcleo de decisão (a parte que mais importa provar).
tenant-contextcompila. Agora a adoção no order-service — leio o teste existente antes de mexer no filtro.Refatoro o filtro do order-service para delegar no núcleo partilhado — mantendo a classe, o construtor antigo e o comportamento (regra never-delete, e os testes existentes continuam válidos).
Dois detalhes que quebrariam a compilação: Spring precisa de saber qual construtor usar, e o teste existente chama
shouldNotFilter(protected noutro pacote). Corrijo ambos.Agora o lado do chamador (product-service) passa a usar o interceptor partilhado, com identidade.
Configuração dos dois serviços: identidade + rotação com janela de sobreposição.
Linha 736 é o
order-service(o que valida) — é lá que a janela de rotação faz sentido.Compilo todos os módulos afetados num só reator.
Todos os módulos compilam e o teste antigo do filtro continua válido (compatibilidade preservada). Acrescento cobertura das capacidades novas ao nível do filtro.
Compila. Agora o closeout documental — o doc S2S afirmava exatamente estas limitações e tem de refletir a realidade nova.
Agora propago a correção aos documentos que repetiam a afirmação antiga, começando pela regra path-scoped do gateway.
Falta o comentário no código que também afirmava a Camada 1 antiga.
Closeout de memória e atualização do grafo.
Tudo compila, grafo atualizado, memória fechada. Relatório final.
Relatório — Hardening S2S (riscos 5 e 6)
1. Implementation summary
A) Camada 1 real —
/internalmorre na borda. NovoInternalPathBlockFilterno gateway:GlobalFilterna ordemHIGHEST_PRECEDENCE + 5— depois doRequestIdFilter(+0), para a rejeição levar correlation id, e antes do JWT (+10), do tenant (+20) e do load-shedding (+30), por isso um caminho bloqueado não gasta parsing de token nem capacidade de shedding.Escolhi 404 e não 403: um 403 confirmaria que o endpoint existe. É o mesmo trade-off que o projeto já tinha fechado na F3 para produtos suspensos (decisão D4) — recurso escondido é indistinguível de inexistente. O match é por
AntPathMatcher, nuncacontains(), senão/api/v1/orders/internallyseria bloqueado por engano (há teste para essa armadilha). Os padrões vivem emgateway.blocked-paths, logo um/internalfuturo fica coberto sem tocar em código.Efeito colateral positivo: cobre também
/api/v1/customers/internal, que tinha a mesma exposição desde muito antes da F7.B) Segredo S2S — rotação, identidade e starter partilhado. Extraí a lógica para
tenant-context/internal/(onde já vive oJwtAuthenticationFilter— o precedente do projeto, não um padrão inventado):acceptedéMap<caller, List<token>>→ vários segredos válidos em simultâneo. Procedimento documentado: antigo emINTERNAL_SERVICE_TOKEN_PREVIOUS→ novo emINTERNAL_SERVICE_TOKEN→ reiniciar o validador → rolar os chamadores → limpar. Valores em branco são descartados, por isso um placeholder por definir é inerte e nunca vira segredo vazio válido.X-Internal-Caller; um segredo válido apresentado com o nome de outro serviço é rejeitado como impersonation.Como respeitei o never-delete:
InternalTokenFiltercontinua a existir — passou a adaptador fino que estende o filtro partilhado, manteve o construtor de segredo único e por isso os 6 testes originais continuam válidos sem alteração. Dois detalhes que teriam quebrado a compilação e apanhei: com dois construtores o Spring não escolhe sozinho (@Autowiredno primário), eshouldNotFilteréprotectedno pai noutro pacote, pelo que o teste do order-service perderia acesso — redeclarei-o só por visibilidade.Autoconfig deliberadamente parcial: só o
InternalTokenAuthenticatoré bean automático. O filtro não é auto-registado (um filtro publicado por biblioteca partilhada entraria na chain de todos os serviços que fizessem upgrade) e o interceptor Feign também não (global, anexaria o segredo interno a todos os clientes de saída).Verificação:
mvn -B test-compile -pl tenant-context,gateway-api-service,order-service,product-service→ BUILD SUCCESS.2. Files changed
Novos (4):
InternalPathBlockFilter.java+ teste (gateway);tenant-context/internal/(InternalTokenAuthenticator,InternalTokenAuthenticationFilter,InternalTokenRequestInterceptor,InternalTokenProperties,InternalTokenAutoConfiguration) +InternalTokenAuthenticatorTest.Modificados — código (5):
InternalTokenFilter.java(adaptador),InternalTokenFilterTest.java(+5 testes),OrderClientConfig.java(interceptor partilhado + caller),OrderSecurityConfig.java(comentário corrigido),AutoConfiguration.imports.Modificados — config (6):
gateway-service.yml(blocked-paths),order-service.yml(internal.accepted+ rotação),product-service.yml(internal.caller/token),gateway messages.properties,docker-compose.ymledocker-compose.prod.yml(INTERNAL_SERVICE_TOKEN_PREVIOUS).Docs/memória (7):
service-to-service-auth.md(§2,3,4,6,7 reescritos; §8 passou de "limitações" a runbook de rotação),.claude/rules/gateway-api-service.md(ordem real da chain + aviso sobrepublic-paths),API.md, gap-analysis, os dois planos,MEMORY.md+ nova memóriaproject_s2s_hardening_2026_07_26.Testes: 13 (authenticator) + 8 (gateway) + 5 (filtro) = 26 novos, todos escritos, nenhum corrido — por regra corres tu.
3. Os bugs foram reproduzidos antes do fix?
Não em runtime, e quero ser explícito. Não corro Docker e a stack não está de pé.
gateway-service.yml:161temPath=/api/v1/orders/**(casa com/internal) epublic-pathsnão inclui/internal. A conclusão "qualquer JWT válido alcançava o endpoint" decorre dessa leitura; não emiti o pedido HTTP que o demonstraria.isValidoriginal comparava um valor contra um valor; a ausência é o código, não há falha a provocar.customer-url,order-url) e nunca o gateway, e o SPA não chama/internalem lado nenhum.4. Remaining risks
tenant-contextobriga a reconstruir TODOS os serviços que o embebem — não só os quatro que compilei. Um rollout parcial deixa serviços com a versão antiga da biblioteca.gateway.blocked-pathsé config, esvazia-se sem deploy de código.PREVIOUSno ExternalSecret de propósito: o ESO falha a sincronizar o Secret inteiro se a property não existir no Vault, e uma entrada opcional permanente seria um passivo permanente. Documentei: durante a rotação acrescenta-se ao Vault e ao chart, depois remove-se.require-caller-header: falsepor defeito para tolerar upgrade parcial — até ser ligado, um token válido sem header passa comounidentified.legacypseudo-caller, que qualquer nome pode usar). Existe para não partir o deployment atual; enquanto lá estiver, dilui a garantia por-caller. Remover por serviço depois da migração.docs/e.claude/continuam em.gitignore(pré-existente): toda esta documentação, incluindo o runbook de rotação, fica fora do controlo de versões.O erro real está escondido:
ApplicationContext failure threshold (1) exceededsignifica que o primeiro context falhou e os restantes só herdam a falha. A causa está na minha alteração ao construtor doInternalTokenFilter. Vou confirmar lendo os testes de slice e como os filtros eram satisfeitos antes.Confirmado o mecanismo.
OrderControllerTesté@WebMvcTestcomaddFilters = false, e já faz@MockBeandoTenantHibernateFilterActivator(linha 69) — precisamente porque um@Componentdo order-service é apanhado pelo slice. Deixa-me ver os outros dois testes que falham e como o filtro era satisfeito antes.Root cause confirmed.
OrderActuatorSecurityTestimportaOrderSecurityConfig(que injetaInternalTokenFilter), eOrderControllerTest/OrderLineControllerTestapanham-no por ser um@Component Filterno pacote base da app. Antes, o construtor primário era(@Value String, MessageSource, ObjectMapper)— sempre construível num slice. A minha alteração tornou o construtor primário dependente do beanInternalTokenAuthenticator, que só existe via auto-config do tenant-context — e os slices@WebMvcTestnão carregam auto-configs. Resultado: o bean falta, o primeiro contexto rebenta, e o "failure threshold (1) exceeded" propaga-se a todos os testes seguintes.Correção: o construtor injetado pelo Spring passa a usar
ObjectProvider<InternalTokenAuthenticator>(sempre injetável), com fallback fail-closed ao segredo legado quando o bean não existe — que é exatamente o caso do slice. Zero edições nos testes, comportamento de produção intacto.Agora corro os testes que falhavam para provar a correção — são os que reportaste, e um fix não é real sem prova.
O order-service, compilado isoladamente, usa o
tenant-contextinstalado no repositório local — que ainda não tem o pacoteinternalnovo. Tenho de instalar otenant-contextprimeiro (a memória já regista isto). Instalo e volto a correr.O meu teste usa o construtor
(InternalTokenAuthenticator, MessageSource, ObjectMapper), que eu substituí peloObjectProvider. Adiciono-o de volta como construtor de conveniência para testes.Todos verdes: 40 testes, 0 failures, 0 errors. Corro agora a suíte completa do order-service para confirmar que não deixei nada partido.
order-service completo: 212 testes, 0 failures. Como o mesmo problema de "reator vs instalado" pode afetar o product-service (mudei
OrderClientConfig) e o gateway (filtro novo), verifico os dois standalone.Tudo verde. Registo a lição na memória e no plano — a raiz é uma armadilha que se repetirá.
Corrigido e verificado com testes a correr de verdade.
Relatório — Regressão nos testes do order-service
1. Root cause
Uma regressão que eu introduzi no hardening S2S. A mensagem
ApplicationContext failure threshold (1) exceededé enganadora: só diz que o primeiro contexto falhou e todos os seguintes herdam a falha — a causa real está escondida.A causa:
InternalTokenFilteré um@Componentdo tipoFilterno pacote base do order-service. Um slice@WebMvcTestinstancia beansFilterno seu scan, mesmo comaddFilters = false. No hardening mudei o construtor primário para exigir o beanInternalTokenAuthenticator, que só é contribuído pela auto-config dotenant-context— e slices@WebMvcTestnão carregam auto-configs. Bean em falta → o primeiro contexto de slice não carrega → threshold excedido → cascata emOrderControllerTest,OrderLineControllerTest,OrderActuatorSecurityTesteInternalPurchaseControllerTest.Antes, o construtor era
(@Value String, MessageSource, ObjectMapper)— sempre construível num slice. (Os filtros@Componentdo própriotenant-context, como oJwtAuthenticationFilter, não mordem porque estão noutro pacote base, fora do scan do slice do order-service; oInternalTokenFiltermorde por estar no mesmo pacote base.)Houve ainda duas causas secundárias que só apareceram ao correr os testes por-módulo (e que documentei):
mvn test -pl order-serviceisolado usa o jar instalado dotenant-context, que não tinha o pacoteinternalnovo →package ...tenantcontext.internal does not exist. Precisa demvn install -pl tenant-contextprimeiro.(InternalTokenAuthenticator, …)tinha sido substituído peloObjectProvider, e o meu próprio teste novo ainda o usava → erro de tipo.2. Implementation summary
InternalTokenFilterpassou aObjectProvider<InternalTokenAuthenticator>(sempre injetável), com fallback fail-closed ao segredo legado quando o bean não existe — exatamente o caso do slice, onde o filtro nunca é invocado. Comportamento de produção intacto (a auto-config continua a fornecer o authenticator rico por-caller).(InternalTokenAuthenticator, MessageSource, ObjectMapper)para os testes.3. Files changed
order-service/.../internal/InternalTokenFilter.java— construtorObjectProvider+ fallback; construtor direto reposto; importsObjectProvider/@Value..claude/memory/project_s2s_hardening_2026_07_26.mdeMEMORY.md— root cause + os dois gotchas registados.Nenhum ficheiro de teste alterado para esta correção.
4. Os bugs reproduziram-se antes do fix?
Sim — reproduzidos e depois provados resolvidos, ambos com testes a correr:
Os 5 testes de
InternalTokenFilterTest(per-caller/rotação) e os 3 deInternalPurchaseControllerTesttambém passam.5. Remaining risks
mvn install -pl tenant-contexté pré-requisito. Qualquer correr de testes/build por-módulo depois de mexer notenant-contexttem de instalar a lib primeiro, senão os módulos usam o jar antigo. Corri oinstall; se limpares o.m2, repete-se.tenant-context— não só os quatro tocados — antes de subir a stack. Um rollout parcial mistura versões da lib.product-servicenem detenant-context, nem os IntegrationTest (Testcontainers). OInternalTokenAuthenticatorTest(13 testes) compila mas não o executei isoladamente.@ComponentFilter/Controller no pacote base cuja dependência venha só de auto-config voltará a partir todos os@WebMvcTestdo módulo. Mitigação registada na memória (ObjectProvider ou@MockBeanpor slice).require-caller-header: false; segredo legado ainda aceite;docs/e.claude/em.gitignore).