# M-2 — реальный Yandex SmartCaptcha драйвер самозаписи

**Дата:** 21.06.2026. **Кодовая фраза стены:** «роутер-наставник».
**Статус:** дизайн одобрен владельцем. Следующий шаг — план (writing-plans) → TDD.
**Связь:** приёмка, находка M-2 ([owner-decisions](2026-06-21-acceptance-owner-decisions.md)).

---

## Проблема

`CaptchaVerifier` биндится на `NullCaptchaVerifier` **безусловно** ([AppServiceProvider.php:54-57](../../../app/app/Providers/AppServiceProvider.php#L54)); реального драйвера нет. На проде (`.env` без CAPTCHA-переменных, сверено 21.06) капча эффективно выключена: любой непустой `captcha_token` проходит → вектор фарма 300₽-бонуса самозаписи. Решение владельца — поставить реальный Yandex SmartCaptcha.

Контракт уже развязан: [`CaptchaVerifier::verify(?string $token, ?string $ip): bool`](../../../app/app/Services/Captcha/CaptchaVerifier.php); зовётся в [`RegistrationService::register`](../../../app/app/Services/Auth/RegistrationService.php) (false → `RegistrationException('captcha_failed')`). Контроллер/сервис менять не нужно — только драйвер + биндинг.

---

## Решение

### 1. `app/app/Services/Captcha/YandexSmartCaptchaVerifier.php` (новый)

`final class YandexSmartCaptchaVerifier implements CaptchaVerifier`. Зависимости — через DI ничего (использует `Http` фасад + `config`).

`verify(?string $token, ?string $ip = null): bool`:
1. Пустой/null `$token` → **false** (без HTTP-вызова — как Null, не тратим запрос на заведомо пустой).
2. POST на `config('services.captcha.yandex_validate_url')` (дефолт `https://smartcaptcha.cloud.yandex.ru/validate`), form-body: `secret` = `config('services.captcha.yandex_server_key')`, `token` = `$token`, `ip` = `$ip` (только если не null). Таймаут 2с.
3. HTTP 200 + JSON `status === 'ok'` → **true**.
4. HTTP 200 + JSON `status === 'failed'` (или иной не-`ok`) → **false** (реальный отказ капчи — НЕ fail-open).
5. Таймаут / connection error / не-200 / нечитаемый JSON → **fail-open: true** + `Log::warning('captcha.yandex_unreachable', …)` (решение владельца: UX > антифрод; throttle регистрации остаётся вторым рубежом).

### 2. `app/app/Providers/AppServiceProvider.php` (правка)

Заменить безусловный биндинг:
```php
$this->app->bind(
    CaptchaVerifier::class,
    fn () => match (config('services.captcha.driver')) {
        'yandex' => new YandexSmartCaptchaVerifier,
        default => new NullCaptchaVerifier,
    },
);
```
`default` ловит `'null'` и любое нераспознанное значение → безопасный Null (dev/test).

### 3. `app/config/services.php` (правка)

В блок `captcha` добавить:
```php
'yandex_validate_url' => env('YANDEX_SMARTCAPTCHA_VALIDATE_URL', 'https://smartcaptcha.cloud.yandex.ru/validate'),
```
`driver` (`CAPTCHA_DRIVER`) и `yandex_server_key` (`YANDEX_SMARTCAPTCHA_SERVER_KEY`) — уже есть.

### 4. `app/.env.example` (правка)

Раскомментировать/добавить ориентиры: `CAPTCHA_DRIVER=null` (на проде → `yandex`), `YANDEX_SMARTCAPTCHA_SERVER_KEY=`.

---

## Тесты (TDD) — `app/tests/Feature/Captcha/YandexSmartCaptchaVerifierTest.php`

`Http::fake` на validate-URL:
1. пустой токен → false, **HTTP не вызван** (`Http::assertNothingSent`).
2. Yandex отвечает `{"status":"ok"}` → true.
3. Yandex отвечает `{"status":"failed"}` → false.
4. Yandex 500 → **true** (fail-open).
5. Yandex connection-timeout (`Http::fake` throw) → **true** (fail-open).
6. запрос несёт `secret`+`token`+`ip` (через `Http::assertSent`).
7. **биндинг**: `config(['services.captcha.driver' => 'yandex'])` → `app(CaptchaVerifier::class)` instanceof `YandexSmartCaptchaVerifier`; `'null'` → `NullCaptchaVerifier`.

---

## Границы / YAGNI

- **Только серверная валидация.** Фронт-виджет SmartCaptcha (site-key, JS) — отдельная фронт-задача, вне этого бэкенд-драйвера.
- **Без изменений схемы/БД.**
- **Без правок контроллера/RegistrationService** — развязаны интерфейсом.
- Драйвер по умолчанию остаётся `null` — поведение dev/test и существующие тесты не меняются.

## Деплой (шаг владельца)

На проде: `CAPTCHA_DRIVER=yandex` + `YANDEX_SMARTCAPTCHA_SERVER_KEY=<server-ключ из Yandex Cloud>` в `.env` → `config:cache` → драйвер активен. Без этого — Null (как сейчас). Claude прод не катит.

## Verification

- Pest `YandexSmartCaptchaVerifierTest` зелёный.
- Регрессия `RegistrationTest`/captcha-тестов — без падений (драйвер по умолчанию Null).
- Pint + Larastan(level 5) = 0 по изменённым файлам.
