# F5.I3.T3 — Rate limiting em 3 camadas + `429` RFC 7807

| Campo | Valor |
|---|---|
| **Fase** | F5 — Borda |
| **Issue** | F5.I3 — Cache, resposta e hardening (plano 18) |
| **kd task** | `task_00uqddh7` |
| **Âncora** | `wiki/implementation/05-borda.md` |
| **Status da revisão** | 🔧 com correções |

## O que foi implementado

- **Borda**: Apache (`RequestReadTimeout`, `LimitRequestBody`, `LimitRequestLine`,
  `LimitRequestFields`, `LimitRequestFieldSize`, `mod_qos`/`mod_evasive` condicionais) e Caddy
  (timeouts + `request_body max_size 110MiB`).
- **App**: `throttleWithRedis` (Valkey) + limitadores nomeados — `open-data` (60/min por IP),
  `lgpd-dsar` (5/min por IP), `media-upload` (120/min por usuário/IP); login do painel por
  **e-mail + IP** (`ThrottlePanelLogin`, só conta falha, `Retry-After`).
- **Formulários**: honeypot (`website` → `prohibited`) + time-trap no DSAR.
- `429` em **RFC 7807** com `Retry-After` e `trace_id` (renderer único).
- Gate `tools/security-check.sh` (borda, perfil `production`, `php.ini`, sessão, headers, debugbar,
  `.env` fora do docroot, `TrustProxies`/`TrustHosts`) — no `make check` e no `gates-selftest`.
- Limite de corpo aceita o **maior hard** de mídia (PDF 100 MiB); o limite por tipo é do app
  (`config/media.php` + `MediaLimits`), não da borda.

## Arquivos para revisar

- `src/core/CoreServiceProvider.php` (limitadores)
- `src/core/Internal/Http/Middleware/ThrottlePanelLogin.php`
- `src/modules/Portal/Lgpd/Logic/Requests/LgpdDsarSubmitRequest.php` + `Routes/portal.php`
- `src/core/Internal/Exceptions/ProblemDetailsRenderer.php`
- `src/services/Media/Support/MediaLimits.php` + `config/media.php`
- `docker/web/app.conf` · `docker/web/Caddyfile` · `docker/php-fpm/php.ini`
- `tools/security-check.sh` · `tools/tests/fixtures/security/invalid/*`
- `tests/Feature/CmsAuthTest.php` · `src/modules/Portal/Lgpd/Test/LgpdTest.php`
- `src/services/Media/Test/MediaLimitsTest.php`

## Pontos de revisão

### PHP 8.4 / Laravel 12
- [ ] `declare(strict_types=1)`; DI por construtor (sem `new`/`app()`/`resolve()` no corpo); PHPStan `max`.
- [ ] Nativo primeiro; sem closure em rota; `env()` só em `config/`; validação em `FormRequest`.

### Determinismo (ADR-001)
- [ ] `Clock`/`IdGen` injetados; ordem explícita; idempotência provada por teste.

### Fronteiras (ADR-013/014/015)
- [ ] Camadas respeitadas; módulo consome `Services\{Name}\{Contracts,DTOs,Enums}\*`; nada de storage fora do Media.

### HTML/a11y (ADR-025) e CSS (ADR-010)
- [ ] Semântica, rótulos, alvos; sem `!important`/inline/CDN; `oklch`/`rem`.

### i18n (ADR-012)
- [ ] Nenhuma string visível fixa — inclusive **títulos de erro lançados em PHP** (o `ModuleI18nTest` só varre views).

### Dados (ADR-007/017) e segurança (ADR-020/021/026/030/035)
- [ ] `text`/`jsonb`/`uuidv7()`; `down()` → `backup_*`; CHECKs; PII cifrada; `throttle`; MIME real; `alt` obrigatório.

### Observabilidade/auditoria (ADR-024/035)
- [ ] Span + `log_event` com o **mesmo** `{service}.{action}`; auditoria via `AuditWriter`; sem PII.

### Específicos desta task

- [ ] Toda rota de escrita tem `throttle:`; `429` com `Retry-After` (login, DSAR, mídia, galeria).
- [ ] Honeypot/time-trap só no formulário público (não em rota de API).
- [ ] Nenhum limite de transporte menor que o maior hard de mídia (`MediaLimitsTest`).
- [ ] Estado do freio no Valkey (nunca DB no caminho quente); `array` nos testes.

## Comandos úteis

```bash
make check              # caminho único (rápido)
make check E2E=1        # + Playwright/axe/perf (2× ao fechar o escopo)
```

---

> Revisar contra `AGENTS.md`, `wiki/adr/` e o plano citado. Registrar achados no `kd` (`kd write --anchor`) e, se houver correção, teste de regressão.

## Notas da revisão

| Data | Revisor | Resultado | Observações |
|---|---|---|---|
| 2026-10-19 | agente (PROTOCOLO v2.0) | 🔧 | **A** (médio, D13) os títulos de erro do freio/limite de mídia vinham **fixos em pt_BR** no PHP
(`CreateMediaBatch`, `CreateMediaUpload`, `GalleryService`, `GalleryBatchCommitController`,
`MediaInputer`) e o `ModuleI18nTest` só varre **views** — painel em `en` mostrava português →
chaves `error_*` nos 4 locales de `media::limits`/`gallery::gallery` + teste (`MediaBatchTest`,
vermelho com o código antigo) — `fact_01j4nvg7`. **Verificado OK**: 3 camadas presentes e no gate
(`security-check`), `429`+`Retry-After`+`trace_id` (CmsAuthTest/LgpdTest), honeypot+time-trap no
DSAR, `MediaLimitsTest` amarrando borda×app, freio em Valkey fora de `testing`. **B** (obs, D11) o
gate de borda valida **texto** dos arquivos: um container `web` rodando config antigo passa no gate
e só o e2e pega (aconteceu nesta sessão — CSP sem Trusted Types) — `risk_01qke8zj`. |

> Resultado: `✅ revisado` · `🔧 com correções` · `⏭️ delegado` (com justificativa).
