# Deep dive #8 — reCAPTCHA v3 antibots (CD + BP)

**Status**: ✅ DONE (2026-05-02)
**Frente master**: #8 NOW
**Tiempo invertido**: ~3 horas (incluye investigación de arquitectura, generación de keys, deploy a 5 ubicaciones, debug de permisos en CD)
**Doc relacionado**: [00-master-roadmap.md](./00-master-roadmap.md)

---

## Situación inicial

- Bots golpeando los formularios de contacto de **lacompaniadigital.com** (CD) y **bewpro.com** (BP).
- Ningún sitio tenía protección antibots — cualquier POST a `/contact` o `/newsletter-subscribe` pasaba sin filtro.
- Sin captcha visible, sin honeypot, sin rate limiting específico al form.

## Decisiones de arquitectura

### 1. reCAPTCHA v3 (no v2)
**Por qué v3**: invisible, score-based, no rompe UX. v2 ("I am not a robot") agrega fricción visible que baja conversión.

### 2. Una sola key para toda la red BewPro
**Decisión**: 1 sitio en Google reCAPTCHA llamado `BewPro Network` que cubre TODOS los dominios (bewpro.com, lacompaniadigital.com, *.bewpro.com, futuros tenants).

**Alternativa descartada**: 1 sitio reCAPTCHA por tenant. Razón: Google permite N dominios por sitio sin penalización de score; tener N sitios separados solo agrega overhead de gestión sin ganancia.

### 3. Implementación en core (codebase compartido)
**Por qué**: cualquier mejora antibots futura aplica automáticamente a TODOS los tenants vía `git pull`. No hay duplicación de código.

### 4. Auto-injection en provisión de tenants nuevos
**Por qué**: cero fricción operativa. El `setup_cd_project4.sh` inyecta `RECAPTCHA_SITE_KEY` y `RECAPTCHA_SECRET_KEY` en el `.env` del tenant nuevo, igual al patrón de `BILLING_PUSH_SECRET`.

### 5. Fail-open en backend
**Por qué**: si Google API tarda/falla, no bloqueamos formularios reales. Score < 0.5 = bloquear; sin token o API timeout = pasar (con log warning).

---

## Arquitectura técnica

### Backend (Laravel core)

```
app/Helpers/Recaptcha.php           ← Verifica token contra Google API
app/Http/Middleware/VerifyRecaptcha  ← Gate en routes; modo dev pass-all si no hay keys
app/Http/Kernel.php                  ← Registro middleware 'recaptcha' alias
config/services.php                  ← Config: site_key, secret, min_score
```

### Frontend (Blade)

```
resources/views/components/recaptcha-script.blade.php  ← Componente auto-attach
resources/views/layout/front/master.blade.php          ← Inyecta <x-recaptcha-script />
```

El componente:
1. Carga `grecaptcha` JS de Google (si SITE_KEY configurada).
2. Auto-attach a forms con clase `js-recaptcha-form` o `contact-form-recaptcha-v3`.
3. En cada submit: `grecaptcha.execute(SITE_KEY, {action})` → token → input hidden `g-recaptcha-response`.
4. Graceful degrade: si SITE_KEY vacía, no inyecta nada.

### Routes gated

```php
// routes/modules/cd-base.php
Route::post('/contact', ...)->middleware('recaptcha:contact');
Route::post('/newsletter-subscribe', ...)->middleware('recaptcha:newsletter');
```

`recaptcha:{action}` — el `action` se valida en backend (debe matchear lo que dispara el frontend).

### Provisioning auto-inject

```bash
# infrastructure/scripts/provisioner/setup_cd_project4.sh
RECAPTCHA_SITE_KEY_VAL="${RECAPTCHA_SITE_KEY:-}"
RECAPTCHA_SECRET_KEY_VAL="${RECAPTCHA_SECRET_KEY:-}"
su - "${USERNAME}" -c "
  cd public_html/git-files/${PROJECT_DIR_NAME} && \
  grep -q '^RECAPTCHA_SITE_KEY=' .env || echo 'RECAPTCHA_SITE_KEY=${RECAPTCHA_SITE_KEY_VAL}' >> .env && \
  grep -q '^RECAPTCHA_SECRET_KEY=' .env || echo 'RECAPTCHA_SECRET_KEY=${RECAPTCHA_SECRET_KEY_VAL}' >> .env
"
```

---

## Keys deployment matrix

| Ubicación | SITE_KEY | SECRET | Verificado |
|-----------|----------|--------|-----------|
| BP `/home/bewpro22/public_html/bewpro/.env` | ✅ | ✅ | HTML contiene script + form attrs |
| CD `/home/lacompany/public_html/git-files/lacompany/.env` | ✅ | ✅ | HTML contiene script + form attrs |
| VPS1 `/root/scripts/.airtable.env` | ✅ | ✅ | (para scripts orquestación) |
| VPS2 `/root/scripts/.airtable.env` | ✅ | ✅ | (para scripts orquestación) |
| Local `.env` | ✅ | (no necesario en dev) | dev pass-all activo |
| Tenants futuros | auto-inject vía setup script | idem | READY |

**Keys actuales** (sitio "BewPro Network" en Google reCAPTCHA console):
- SITE_KEY: `6LeK6dUsAAAAAMR8c7yTgISJwCkJ9wshup4wQs3M` (público, va en HTML)
- SECRET: vive solo en `.env` de cada server, nunca commitear

---

## Caso particular: deploy en CD

CD se ejecuta sobre el mismo codebase que cualquier tenant, **pero arrastra deuda operativa**:
- Repo en commit `cb106424` (varios meses atrás).
- ~30 archivos modificados localmente sin commit (parches in-situ).
- Permisos rotos: muchos archivos `root:root` en vez de `lacompany:lacompany`.
- `git pull` falla porque archivos como `.claude/settings.local.json` y `docs/_legacy/*` son root-owned.

### Workaround aplicado (parche quirúrgico)

1. `master.blade.php` editado con `sed` para inyectar `<x-recaptcha-script />` antes de `</body>`.
2. Componente Blade copiado por SCP+SSH a `resources/views/components/recaptcha-script.blade.php`.
3. `view:clear`, `config:clear`.

### Deuda registrada

CD requiere a futuro (no urgente):
- `chown -R lacompany:lacompany` para destrabar git pull.
- Decidir destino de los ~30 archivos modificados (commitear si valen, descartar si no).
- Pull al HEAD actual del repo.

Ver decisión en `decisions-log.md`: "CD se mantiene a mano vs como tenant productizado".

---

## Validación end-to-end

### BP

```bash
curl -s https://bewpro.com/contact | grep -o 'recaptcha/api.js?render=[^"]*'
# → recaptcha/api.js?render=6LeK6dUsAAAAAMR8c7yTgISJwCkJ9wshup4wQs3M
```

### CD

```bash
curl -s https://lacompaniadigital.com/contact | grep -c "recaptcha"
# → 16

curl -s https://lacompaniadigital.com/contact | grep -o 'js-recaptcha-form\|contact-form-recaptcha-v3'
# → contact-form-recaptcha-v3 (auto-attach activo)
```

Ambos sitios HTTP 200, sin Server Error, script de Google cargando, form classes detectadas.

---

## Próximos pasos (no urgentes)

| Acción | Cuándo | Por qué |
|--------|--------|---------|
| Monitorear logs de submits 1 semana | semana del 2026-05-09 | Confirmar que score-based reduce bots a ~0 |
| Ajustar `RECAPTCHA_MIN_SCORE` si bloquea humanos | si hay reportes | Default 0.5 puede ser muy estricto en mobile |
| Bulk apply a ~45 tenants legacy | cuando haya señal de bots ahí | "En caso de recibir reportes ajustaremos" — diferido |
| Limpiar deuda CD (permisos + git pull) | cuando haya 30 min ociosos | Destraba CD para deploys automatizados |

---

## Anatomía del flow request

```
Usuario submit form en /contact
   ↓
Frontend: grecaptcha.execute(SITE_KEY, {action: 'contact'})
   ↓
Google API → token JWT
   ↓
JS injecta hidden input g-recaptcha-response = token
   ↓
form.submit() POST /contact
   ↓
Middleware 'recaptcha:contact' intercepta
   ↓
Recaptcha::verify($token, 'contact')
   ↓
POST a https://www.google.com/recaptcha/api/siteverify
   ↓
Google responde: {success: true, score: 0.9, action: 'contact'}
   ↓
score >= 0.5 ✓ → next($request)
score < 0.5 ✗ → 422 / back() con error
   ↓
Controller normal procesa el form
```

---

## Decisiones registradas

- **v3 vs v2**: v3 (invisible, no fricción de UX).
- **1 sitio Google vs N sitios**: 1 sitio cubriendo toda la red bewpro.
- **Helper + middleware en core**: reusable por cualquier tenant sin duplicación.
- **Fail-open en backend**: prioridad UX > seguridad estricta (bots de score bajo se filtran igual).
- **CD parche quirúrgico vs limpieza completa**: parche quirúrgico ahora, limpieza diferida.

---

*Cerrado: 2026-05-02*
