diff --git a/architectory/arch-03-docker-compose-blueprint.md b/architectory/arch-03-docker-compose-blueprint.md
index 4821415..1f24070 100644
--- a/architectory/arch-03-docker-compose-blueprint.md
+++ b/architectory/arch-03-docker-compose-blueprint.md
@@ -240,7 +240,7 @@ Identity provider. **Обязателен** в compose-контуре с пер
- включены proxy settings для работы за `nginx`;
- импорт realm в local/dev;
- использует managed PostgreSQL, схема `keycloak` (см. раздел «Managed PostgreSQL» выше);
-- OTP mock / SMS SPI — см. arch-04; real mode вызывает только `sms-service` по сети `backend`, сам Keycloak к Direct/`egress` не подключён;
+- OTP mock / SMS SPI — см. arch-04; real mode вызывает `sms-service` по сети `backend`, а единственный утверждённый внешний вызов Keycloak через `egress` — server-side validation Yandex SmartCaptcha;
- healthcheck;
- взаимодействия — [`arch-02-api-contracts.md`](arch-02-api-contracts.md), «Frontend ↔ Keycloak», и [`arch-01-system-architecture.md`](arch-01-system-architecture.md), «Keycloak».
@@ -282,7 +282,7 @@ Identity provider. **Обязателен** в compose-контуре с пер
- `public`: `nginx`, `keycloak` (для прокси `/auth/*`), frontend static/dev access, внешний HTTPS entrypoint.
- `backend`: `api-backend`, `message-safety`, `bitrix-sync`, `bitrix-local-app`, `keycloak`, `redis` (managed PostgreSQL — вне compose, в VPC).
-- `egress`: только сервисы с утверждёнными исходящими интеграциями; для SMS — `sms-worker`, но не Keycloak. Production real mode требует фактический статический egress IP/NAT, записанный в inventory и переданный Direct для allowlist.
+- `egress`: только сервисы с утверждёнными исходящими интеграциями; `sms-worker` обращается к Direct, Keycloak — только к `smartcaptcha.cloud.yandex.ru` для server-side validation. Production real mode требует фактический статический egress IP/NAT, записанный в inventory и переданный Direct для allowlist.
- `observability`: `otel-collector` + сервисы, экспортирующие telemetry.
Базы данных, Redis, Keycloak internal port и API internal port не должны публиковаться наружу. `message-safety` доступен только внутри сети `backend`. Основной пользовательский путь должен идти через `nginx` и HTTPS.
diff --git a/architectory/arch-04-settings-and-content.md b/architectory/arch-04-settings-and-content.md
index f15b706..3f514c6 100644
--- a/architectory/arch-04-settings-and-content.md
+++ b/architectory/arch-04-settings-and-content.md
@@ -29,6 +29,7 @@ Managed PostgreSQL **поднимается до** развёртывания п
- параметры **nginx/TLS** и edge rate limits (`NGINX_RATE_LIMIT_*`);
- идентификация Keycloak: realm, audience, public/internal URL;
- переключатель и секрет временного OTP mock (`KEYCLOAK_OTP_MOCK_*`); mock обязателен до прохождения real-SMS rollout gates и запрещён как незаявленный fallback;
+- переключатель и client/server keys Yandex SmartCaptcha (`KEYCLOAK_YANDEX_CAPTCHA_*`); сложность остаётся в Yandex Cloud, а server key не попадает в тему/логи;
- технические параметры сервисов, пока профильная спецификация не определила service-owned settings; для `sms-service` runtime-параметры уже вынесены в `sms.sms_setting`.
**Запрещено в `.env` (→ только `app_settings`):**
@@ -226,6 +227,9 @@ KEYCLOAK_REALM=han-chat
KEYCLOAK_AUDIENCE=han-chat-api
KEYCLOAK_OTP_MOCK_ENABLED=true
KEYCLOAK_OTP_MOCK_CODE=1234
+KEYCLOAK_YANDEX_CAPTCHA_ENABLED=false
+KEYCLOAK_YANDEX_CAPTCHA_CLIENT_KEY=
+KEYCLOAK_YANDEX_CAPTCHA_SERVER_KEY=
KEYCLOAK_SMS_SERVICE_URL=http://sms-service:8080
# =============================================================================
diff --git a/backlog.md b/backlog.md
index a7a7dd0..c117631 100644
--- a/backlog.md
+++ b/backlog.md
@@ -17,6 +17,8 @@
16. Сделать тестового пользователя с фиксированным СМС-входом
~~17. Формы согласий поправить (Согласие на обработку ПД + Политика, Пользовательское соглашение, Реклама)~~
~~18. При повторном запросе OTP кода при авторизации не нужно указывать ошибку "Новый код заказан. Предыдущий код больше не действует."~~
+19. Хранить историю устройств, с которых пользователь входил в ЛК (Ид юзера, идентификатор устройства, дата последнего входа, способ входа - веб\приложение)
+20. Убрать с экрана при запросе OTP тексты согласий (внизу экрана)
На будущее (после доработки отдельных функциональностей):
1. Разработка message-safety
diff --git a/codebase/backend/.env.example b/codebase/backend/.env.example
index b809af1..b4a8880 100644
--- a/codebase/backend/.env.example
+++ b/codebase/backend/.env.example
@@ -68,6 +68,10 @@ KEYCLOAK_AUDIENCE=han-chat-api
KEYCLOAK_OTP_MOCK_ENABLED=true
KEYCLOAK_OTP_MOCK_CODE=change-me
KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=false
+KEYCLOAK_YANDEX_CAPTCHA_ENABLED=false
+# Обязательны только при KEYCLOAK_YANDEX_CAPTCHA_ENABLED=true.
+KEYCLOAK_YANDEX_CAPTCHA_CLIENT_KEY=
+KEYCLOAK_YANDEX_CAPTCHA_SERVER_KEY=
# (openssl rand -hex 32)
KEYCLOAK_OTP_HMAC_KEY=change-me
KEYCLOAK_OTP_SETTINGS_MAX_STALE_SEC=300
diff --git a/codebase/backend/deployment/RUNBOOK.ru.md b/codebase/backend/deployment/RUNBOOK.ru.md
index 60e9e65..e5687f0 100644
--- a/codebase/backend/deployment/RUNBOOK.ru.md
+++ b/codebase/backend/deployment/RUNBOOK.ru.md
@@ -138,8 +138,26 @@ docker compose ps keycloak
- [ ] Issuer discovery/JWKS точно совпадает с публичным HTTPS URL `/auth`.
- [ ] Frontend-клиент является публичным PKCE S256; implicit, password и social flows отключены.
- [ ] Неверный или повторно использованный OTP и превышение лимитов безопасно отклоняются; settings bridge работает fail-closed.
+- [ ] При `KEYCLOAK_YANDEX_CAPTCHA_ENABLED=true` initial send и resend требуют свежий SmartCaptcha token; техническая недоступность Yandex подтверждена как fail-open в логах.
+- [ ] CSP login-страницы содержит `smartcaptcha.cloud.yandex.ru`/`yastatic.net`, а `/auth/realms/master/protocol/openid-connect/3p-cookies/step2.html` и Admin Console работают без CAPTCHA CSP.
- [ ] Временный администратор удален либо его пароль изменен; для именного администратора включена MFA.
+Если предыдущая попытка сохранила custom CSP в realm, сбросьте только это поле через `kcadm`; `.env` как shell-файл не загружать:
+
+```sh
+docker compose exec -T keycloak sh -lc '
+set -eu
+cfg=/tmp/han-kcadm.config
+/opt/keycloak/bin/kcadm.sh config credentials --config "$cfg" \
+ --server http://127.0.0.1:8080/auth --realm master \
+ --user "$KC_BOOTSTRAP_ADMIN_USERNAME" \
+ --password "$KC_BOOTSTRAP_ADMIN_PASSWORD"
+/opt/keycloak/bin/kcadm.sh update realms/han-chat --config "$cfg" \
+ -s "browserSecurityHeaders.contentSecurityPolicy="
+rm -f "$cfg"
+'
+```
+
## Этап 12 — последовательный запуск и готовность
```sh
diff --git a/codebase/backend/frontend-test-site/README.md b/codebase/backend/frontend-test-site/README.md
index 9a66248..be73dab 100644
--- a/codebase/backend/frontend-test-site/README.md
+++ b/codebase/backend/frontend-test-site/README.md
@@ -18,10 +18,10 @@ npm run web
| Файл | Размер |
|---|---|
-| `favicon.png` | 64×64 |
-| `icon-192.png` | 360×360 |
-| `icon-512.png` | 1024×1024 |
-| `apple-touch-icon.png` | 360×360 |
+| `favicon.png` | 32×32 |
+| `icon-192.png` | 192×192 |
+| `icon-512.png` | 512×512 |
+| `apple-touch-icon.png` | 180×180 |
Manifest: `public/manifest.json`. Корневой HTML: `app/+html.tsx`.
@@ -34,7 +34,7 @@ Chrome DevTools → Application → Manifest / Service Workers. API и `/auth/`
## Production
-`npm run build:pwa` создаёт `dist/` с manifest, иконками и `service-worker.js`. Каталог монтируется в корневой nginx; отдельный frontend nginx не используется. Для SPA nginx должен применять `try_files $uri /index.html`, не кэшировать `index.html`, `manifest.json`, `service-worker.js` и бессрочно кэшировать hashed assets.
+`npm run build:pwa` создаёт `dist/` с manifest, иконками и `service-worker.js`. Каталог монтируется в корневой nginx; отдельный frontend nginx не используется. Для SPA nginx должен применять `try_files $uri /index.html`, не кэшировать `index.html`, `manifest.json`, `register-sw.js`, `service-worker.js` и бессрочно кэшировать hashed assets.
Dockerfile собирает статический OCI-артефакт `/dist` без runtime-сервера:
diff --git a/codebase/backend/frontend-test-site/app/+html.tsx b/codebase/backend/frontend-test-site/app/+html.tsx
index c35b697..59c3c87 100644
--- a/codebase/backend/frontend-test-site/app/+html.tsx
+++ b/codebase/backend/frontend-test-site/app/+html.tsx
@@ -1,14 +1,6 @@
import { ScrollViewStyleReset } from "expo-router/html";
import type { PropsWithChildren } from "react";
-const serviceWorkerBootstrap = `
-if ('serviceWorker' in navigator) {
- window.addEventListener('load', () => {
- navigator.serviceWorker.register('/service-worker.js').catch(() => {});
- });
-}
-`;
-
export default function Root({ children }: PropsWithChildren) {
return (
@@ -19,9 +11,9 @@ export default function Root({ children }: PropsWithChildren) {
-
-
-
+
+
+