> ## Documentation Index
> Fetch the complete documentation index at: https://veriqa.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Настройка каналов

> Сквозная настройка четырёх поставляемых каналов: MAX, Telegram, WhatsApp и Email.

Канал — это доверенное приложение, где пользователь подтверждает вход. Veriqa поставляет четыре
адаптера — **MAX**, **Telegram**, **WhatsApp** и **Email** — и настройка идентична, запускаете ли вы
Veriqa [встроенно](/docs/ru/quickstart/dotnet) или [self-hosted](/docs/ru/quickstart/self-hosted).

Каждый канал проходит одни и те же три шага: зарегистрировать адаптер в коде, заполнить его секцию
конфигурации `Veriqa:Channels:{Channel}` и сделать его вебхук доступным из интернета.

```csharp Program.cs theme={null}
adapters.AddTelegram();
adapters.AddWhatsApp();
adapters.AddMax();
adapters.AddEmail();
```

<Note>
  **Каждый канал — свой пакет.** Базовый набор ставится одним метапакетом
  `Veriqa.Core.BaseChannels` (Telegram, WhatsApp, Email); MAX в него не входит и ставится явно —
  `dotnet add package Veriqa.Core.ChannelAdapter.Max`. Нужен один канал — ставьте только его пакет
  (`Veriqa.Core.ChannelAdapter.Telegram`, `.WhatsApp`, `.Max`, `.Email`): вместе с ним приедет
  только его SDK, и почтовая библиотека не окажется в установке, которая пускает пользователей
  через Telegram. Вызов регистрации от выбора пакета не зависит — `adapters.AddTelegram()` пишется
  одинаково в обоих случаях. Если пакета канала нет, а `Add*()` вызван — это ошибка компиляции,
  а не молчаливо отсутствующий канал.
</Note>

Регистрация адаптера — не то же самое, что его включение. Адаптер активируется только когда его
секция несёт `"Enabled": true` — иначе регистрируются лишь его опции и стартовый валидатор, а канал
никогда не появляется на странице входа. Добавляйте только те каналы, которые используете.

<Warning>
  Токены ботов, access-токены и SMTP-пароли — секреты. Держите их в user-secrets в разработке и в
  secret-store в продакшне — никогда в закоммиченном `appsettings.json`.
</Warning>

## Уровень проверки входящего события

У каждой канальной секции есть обязательный ключ `InboundVerification` — он объявляет, **как
проверяется подлинность входящего события этого канала**. Факт объявляете вы: Veriqa видит лишь то,
что проверка выполнилась и прошла, но не то, чем именно она была.

<Warning>
  Развёртывание, где включён поставляемый канал, а значение не задано или не входит в словарь,
  **не стартует**. Проставьте значение и в секциях выключенных каналов — тогда
  переключение `Enabled` в `true` не уронит старт. Стартовая проверка перебирает тот же набор
  каналов, из которого ядро строит список включённых: все четыре поставляемых канала в нём есть, а
  канал стороннего адаптера, подключённый одним вызовом `AddChannel`, — нет: ключ его секции
  обязателен, но на старте не проверяется, см.
  [Свой канальный адаптер](/docs/ru/guides/custom-channel-adapter).
</Warning>

Значения для поставляемых каналов — по режиму, в котором вы их запускаете:

| Канал | Режим | Значение | Чем обеспечено |
| - | - | - | - |
| Telegram | `UpdateMode: Webhook` | `shared_secret` | Секрет заголовка `X-Telegram-Bot-Api-Secret-Token`, сравнение за константное время |
| Telegram | `UpdateMode: Polling` | `outbound_fetch` | Входящих запросов нет: обновления забирает сам сервис |
| MAX | `UpdateMode: Webhook` | `shared_secret` | Секрет заголовка `X-Max-Bot-Api-Secret`, сравнение хешей за константное время |
| MAX | `UpdateMode: Polling` | `outbound_fetch` | Входящих запросов нет: обновления забирает сам сервис |
| WhatsApp (Meta Cloud API) | вебхук | `signature` | HMAC-SHA256 тела запроса, заголовок `X-Hub-Signature-256` |
| WhatsApp (Twilio) | вебхук | `signature` | HMAC-SHA1 над URL вебхука и отсортированными параметрами формы, заголовок `X-Twilio-Signature` |
| Email | `PushEnabled: true` | `shared_secret` | Секрет входящего вебхука проверяют собственные эндпоинты адаптера |
| Email | `PushEnabled: false` (умолчание) | `shared_secret` | Входящего запроса от платформы в Pull-режиме нет вовсе; значение оставлено тем же, что и в Push-режиме |

Канал в режиме `Polling`, которому объявили что-то кроме `outbound_fetch`, даёт `Warning` при
старте — старт при этом продолжается. Полный словарь значений — в
[справочнике конфигурации](/docs/ru/reference/configuration).

## Telegram

**Что нужно:** бот, созданный через [@BotFather](https://t.me/BotFather), и его токен.

```json appsettings.json theme={null}
{
  "Veriqa": {
    "Channels": {
      "Telegram": {
        "Enabled": true,
        "InboundVerification": "shared_secret",
        "BotToken": "YOUR_BOT_TOKEN",
        "BotUsername": "your_login_bot",
        "UpdateMode": "Webhook",
        "WebhookBaseUrl": "https://auth.your-domain.com",
        "WebhookSecretToken": "YOUR_RANDOM_SECRET"
      }
    }
  }
}
```

| Ключ | Примечания |
| - | - |
| `UpdateMode` | `Webhook` (по умолчанию) или `Polling` |
| `WebhookBaseUrl` | Публичный HTTPS base URL хоста Veriqa — обязателен в режиме `Webhook` |
| `WebhookSecretToken` | Произвольная случайная строка; Telegram возвращает её в каждом update |
| `BotUsername` | Используется для построения `t.me` deep-link за QR-кодом |
| `DeepLinkBaseUrl` | По умолчанию `https://t.me/`; переопределяйте только для кастомного deep-link хоста |

В режиме `Webhook` Veriqa **регистрирует вебхук сам** на старте (и снимает при остановке) — вы не
вызываете `setWebhook` вручную. Эндпоинт — `POST {WebhookBaseUrl}/api/channels/telegram/webhook`, и
каждый запрос аутентифицируется заголовком `X-Telegram-Bot-Api-Secret-Token` против
`WebhookSecretToken`.

<Tip>
  Режим `Polling` не требует публичного URL, что делает его практичным выбором для локальной
  разработки — переключайтесь на `Webhook` для всего задеплоенного. Ещё он допускает **только
  одного опрашивающего на бота**: Bot API отдаёт очередь апдейтов единственному клиенту
  `getUpdates`, поэтому второй процесс на том же токене не получает ничего, а его цикл опроса
  повторяет отказ `409` Bot API — в логе он записан как
  `Conflict: terminated by other getUpdates request`, без номера. Отчёт здоровья этого не ловит —
  токен валиден, и канал считает себя `healthy`
  ([запуск более одного инстанса](/docs/ru/quickstart/self-hosted#запуск-более-одного-инстанса)).
</Tip>

## WhatsApp

**Что нужно** (Meta Cloud API, провайдер по умолчанию): Meta-приложение с WhatsApp Business, phone
number ID и постоянный access-токен из консоли [Meta for Developers](https://developers.facebook.com/).
Доставка через Twilio настраивается [ниже](#доставка-через-twilio).

```json appsettings.json theme={null}
{
  "Veriqa": {
    "Channels": {
      "WhatsApp": {
        "Enabled": true,
        "InboundVerification": "signature",
        "Provider": "MetaCloudApi",
        "BusinessPhoneNumber": "+15551234567",
        "MetaCloudApi": {
          "PhoneNumberId": "YOUR_PHONE_NUMBER_ID",
          "AccessToken": "YOUR_ACCESS_TOKEN",
          "AppSecret": "YOUR_APP_SECRET",
          "WebhookVerifyToken": "YOUR_VERIFY_TOKEN",
          "GraphApiVersion": "v21.0"
        }
      }
    }
  }
}
```

`BusinessPhoneNumber` — номер, которому пишут пользователи, в формате E.164 — именно на него
указывают deep-link и QR-код.

В отличие от Telegram, вебхук регистрируется **на стороне Meta**. В панели приложения WhatsApp →
Configuration задайте callback URL `https://auth.your-domain.com/api/channels/whatsapp/webhook`,
вставьте тот же `WebhookVerifyToken` и подпишитесь на поле `messages`. Meta верифицирует URL
запросом `GET` с `hub.mode` и `hub.verify_token`; Veriqa отвечает на него автоматически, как только
канал включён и хост публично доступен.

Каждый последующий `POST` подписывается Meta заголовком `X-Hub-Signature-256` и валидируется против
`AppSecret` — отсутствующий или неверный `AppSecret` означает, что валидные доставки отвергаются.

<Note>
  С каналом поставляются два провайдера доставки: **Meta Cloud API** (`MetaCloudApi`, по умолчанию) и
  **Twilio** (`Twilio`). Ключ `Provider` выбирает, какой из них регистрируется, и объявляет, какого
  провайдера ожидает установка; на старте он сверяется с фактически зарегистрированным. Собственный
  провайдер, подключённый через `UseWhatsAppProvider<TProvider>()`, побеждает поставляемый, и в
  `Provider` ставится его код. Расхождение останавливает хост с ошибкой, называющей оба кода.
</Note>

### Доставка через Twilio

**Что нужно:** аккаунт Twilio (Account SID и Auth Token), WhatsApp-отправитель — Twilio Sandbox for
WhatsApp, чтобы попробовать, и зарегистрированный отправитель в production, — и контент
`twilio/quick-reply`, созданный в Twilio Content Template Builder.

```json appsettings.json theme={null}
{
  "Veriqa": {
    "Channels": {
      "WhatsApp": {
        "Enabled": true,
        "InboundVerification": "signature",
        "Provider": "Twilio",
        "BusinessPhoneNumber": "+15551234567",
        "Twilio": {
          "AccountSid": "YOUR_ACCOUNT_SID",
          "AuthToken": "YOUR_AUTH_TOKEN",
          "QuickReplyContentSid": "YOUR_CONTENT_SID",
          "WebhookUrl": "https://auth.your-domain.com/api/channels/whatsapp/webhook"
        }
      }
    }
  }
}
```

`BusinessPhoneNumber` — номер отправителя Twilio (при Sandbox — номер песочницы). При
`Provider` = `Twilio` обязательны `AccountSid`, `AuthToken`, `QuickReplyContentSid` и `WebhookUrl`:
без любого из них хост не стартует, а ошибка называет ключ, но не его значение. `ApiBaseUrl`
необязателен, по умолчанию `https://api.twilio.com`. Тенант хранит креды Twilio в той же группе кредов
WhatsApp, что и креды Meta.

**Контент quick-reply.** Вопрос подтверждения уходит одним контентом с двумя кнопками. Создайте его
типа `twilio/quick-reply` ровно с этими переменными — Veriqa заполняет их сама:

| Переменная | Чем заполняется |
| - | - |
| `1` | текст вопроса (до 1024 символов; более длинный не отправляется) |
| `2` | подпись кнопки подтверждения (обрезается до 20 символов) |
| `3` | id кнопки подтверждения |
| `4` | подпись кнопки отказа (обрезается до 20 символов) |
| `5` | id кнопки отказа |

```json twilio/quick-reply theme={null}
{
  "twilio/quick-reply": {
    "body": "{{1}}",
    "actions": [
      { "title": "{{2}}", "id": "{{3}}" },
      { "title": "{{4}}", "id": "{{5}}" }
    ]
  }
}
```

Вопрос отвечает на сообщение самого пользователя, поэтому уходит внутри 24-часовой сессии, и
одобрение шаблона WhatsApp ему не нужно. Не отправляйте этот контент на одобрение.

**Вебхук.** В консоли Twilio задайте URL входящих сообщений отправителя (у Sandbox — *When a message
comes in*, метод `POST`): `https://auth.your-domain.com/api/channels/whatsapp/webhook`. Twilio
подписывает каждый запрос заголовком `X-Twilio-Signature`, посчитанным над URL, который знает
**он**, поэтому `WebhookUrl` должен совпадать с этим URL символ в символ — схема, хост, порт, путь и
query. За прокси Veriqa видит свой внутренний адрес, поэтому URL берётся из конфигурации, а не из
запроса; расхождение отвергает каждое входящее сообщение с `403`. Тенант со своим маршрутом вебхука
указывает свой `WebhookUrl`. `GET`-рукопожатия у Twilio нет: `GET` на вебхук отвергается.

При Sandbox каждый тестовый телефон один раз присоединяется к песочнице, отправив на её номер
`join <keyword>`; ключевое слово показано в консоли Twilio.

Этот провайдер работает внутри Veriqa. Пример `node/custom-channel-whatsapp-twilio` — другое: внешний
адаптер, работающий отдельным сервисом под собственным типом канала, см.
[HTTP-адаптер канала на любом языке](/docs/ru/guides/custom-channel-adapter#http-адаптер-канала-на-любом-языке).

### Текст предзаполненного сообщения

WhatsApp — единственный из поставляемых каналов, где deep link несёт **текст сообщения**, а не
служебный параметр: пользователь видит его в поле ввода и отправляет как обычное сообщение. Из
коробки это короткое многострочное сообщение с кодом входа на последней строке:

```text Базовый текст (en) theme={null}
Sign in to Acme
Send this message to sign in.

Access code
auth_7Ks9mR3xYpL2nWq4tBvC8dEfG1hJkM5nP0qRsTuVwXy
```

Текст настраивается **там же, где остальные тексты каналов** — в locale-файлах вашего хоста
(`wwwroot/locales/{язык}.json`, см. [Локализация](/docs/ru/guides/customization#локализация)). Ключей два —
полный вариант с именем приложения и запасной без него; второй используется, когда имя приложения
недоступно. Отдельной секции в `Veriqa:Channels:WhatsApp` для текста нет.

```json wwwroot/locales/ru.json theme={null}
{
  "Sign in to {app}\nSend this message to sign in.\n\nAccess code\n{code}":
    "Вход в {app}\nОтправьте это сообщение для входа.\n\nКод доступа\n{code}",
  "Send this message to sign in.\n\nAccess code\n{code}":
    "Отправьте это сообщение для входа.\n\nКод доступа\n{code}"
}
```

Свой текст задаётся значением этих ключей (для английского — правкой `en.json`). Доступны два
слота: `{app}` — имя приложения, к которому идёт вход, и `{code}` — код входа `auth_…`. Язык
берётся от страницы входа, поэтому переводить стоит все языки, которые вы поставляете.

Имя приложения приходит из регистрации вашего OIDC-клиента и подставляется **как есть** —
Veriqa его не сокращает (общий предел значения слота — 128 символов). Держите имя коротким:
длинное ломает вёрстку сообщения и целиком уходит в QR-код, увеличивая его плотность
(см. предупреждение ниже).

<Warning>
  **Длина текста напрямую влияет на читаемость QR-кода.** В QR кодируется весь `wa.me`-URL, а
  текст в нём percent-кодируется: пробел и перевод строки стоят 3 символа, буква кириллицы — 6,
  символ псевдографики вроде `═` — 9. Чем длиннее текст, тем выше версия QR и тем мельче его
  модули при том же размере картинки на экране.
</Warning>

Отсюда правила оформления, которые стоит соблюдать:

* **код — последней отдельной строкой**: он длиной 48 символов (`auth_` + идентификатор транзакции
  на 43 символа) и не влезает ни в какую рамку;
* **не использовать `*`, `_`, `~`** как элементы графики — WhatsApp трактует их как разметку
  (жирный, курсив, зачёркнутый). Безопасны `-`, `.`, `:`, `+`, `|`;
* **рамки с вертикальными палками** (`|…|`) выравниваются только внутри моноширинного блока
  (тройные бэктики) — в обычном сообщении шрифт пропорциональный и рамка разъедется. Часть
  клиентов к тому же показывает в поле ввода сами бэктики, а моноширинность — уже в отправленном
  сообщении, поэтому такое оформление проверяйте на устройстве до выкладки;
* **горизонтальные линейки** (`------`) работают в любом клиенте и обёртки не требуют.

Ориентиры цены оформления (генератор QR из поставки Veriqa; телефон 11 цифр, идентификатор
транзакции 43 символа, уровень коррекции M, имя приложения «Acme Corp»). Колонка «px на модуль» —
размер одного модуля QR при штатном боксе 220 px на странице входа. Порог сканируемости —
**2,86 px на модуль**. Строки помечены языком текста — нелатинский алфавит стоит заметно дороже за
символ:

| Оформление | Язык текста | Длина URL | Версия QR | px/модуль @220 | Пригодно для QR |
| - | - | - | - | - | - |
| Без графики — вариант по умолчанию | английский | 171 | v9 | 3.61 | Да, с запасом |
| Без графики — вариант по умолчанию | русский | 388 | v13 | 2.86 | Да, ровно на пороге |
| Линейки из дефисов + центрированный заголовок | русский | 439 | v13 | 2.86 | Да, ровно на пороге |
| Линейки из дефисов | русский | 520 | v15 | 2.59 | Нет — ниже порога |
| Рамка `+--+` | английский | 355 | v13 | 2.86 | Нет — рамке нужен моноширинный блок |
| Рамка `+--+` | русский | 682 | v17 | 2.37 | Нет |
| Рамка Unicode `╔══╗` | русский | 1147 | v23 | 1.88 | Нет |

Русские варианты без графики и с линейками-заголовком стоят **ровно на пороге** — запаса у них
нет: любое удлинение текста поднимает версию QR и выводит код из нормы. Английский вариант по
умолчанию проходит с запасом. У вариантов с рамкой гейтов два: русские рамки не берут ещё и порог
плотности (2.37 и 1.88), а моноширинный блок нужен любой рамке — даже английская, стоящая ровно на
пороге, в обычном сообщении разъедется. Отсюда «Нет» в колонке.

Практический вывод: если сообщение показывается **и** как QR-код, держитесь варианта без графики
или ограничьтесь горизонтальными линейками — рамки и псевдографика делают QR-код трудным для
камеры. Русский текст обходится примерно вдвое дороже английского при том же оформлении: тот же
текст по умолчанию стоит 388 символов против 171.

Готовые образцы каждого варианта — ниже; `{app}` и `{code}` подставит Veriqa.

```text Без графики — вариант по умолчанию theme={null}
Вход в {app}
Отправьте это сообщение для входа.

Код доступа
{code}
```

```text Линейки из дефисов + центрированный заголовок theme={null}
------------------------
   ВХОД В {app}
------------------------
Просто отправьте это сообщение.

Код доступа:
{code}
```

```text Линейки из дефисов theme={null}
------------------------
ВХОД В {app}
------------------------
Отправьте это сообщение,
чтобы подтвердить вход.

Код доступа
{code}
```

Варианты с рамкой требуют моноширинного блока — сам текст обёрнут в тройные бэктики, а код
вынесен под блок (внутрь рамки строка кода не влезает):

````text Рамка «+--+» — только после проверки на устройстве theme={null}
```
+----------------------+
|  ВХОД В {app}        |
+----------------------+
| Отправьте это        |
| сообщение, чтобы     |
| подтвердить вход.    |
+----------------------+
```
Код доступа
{code}
````

````text Рамка Unicode — самый дорогой вариант, для QR непригоден theme={null}
```
╔══════════════════════╗
║  ВХОД В {app}        ║
╠══════════════════════╣
║ Отправьте это        ║
║ сообщение.           ║
╚══════════════════════╝
```
Код доступа
{code}
````

Выбранный образец переносится в locale-файл одной строкой — переводы строк записываются как `\n`:

```json wwwroot/locales/ru.json — вариант с линейками theme={null}
{
  "Sign in to {app}\nSend this message to sign in.\n\nAccess code\n{code}":
    "------------------------\n   ВХОД В {app}\n------------------------\nПросто отправьте это сообщение.\n\nКод доступа:\n{code}",
  "Send this message to sign in.\n\nAccess code\n{code}":
    "------------------------\nОтправьте это сообщение для входа.\n------------------------\n\nКод доступа:\n{code}"
}
```

<Note>
  Оформляйте **оба** ключа сразу. Второй — запасной вариант без имени приложения; если оформить
  только первый, пользователи, у которых имя приложения недоступно, увидят другое сообщение.
  Выравнивание по `{app}` — приблизительное: имя приложения у каждого клиента своей длины, поэтому
  ни центрирование пробелами, ни правая граница рамки не сойдутся точно. Если рамка нужна ровной —
  берите вариант без имени приложения.
</Note>

## MAX

**Что нужно:** бот MAX и его токен. Конфигурация зеркалит Telegram:

```json appsettings.json theme={null}
{
  "Veriqa": {
    "Channels": {
      "Max": {
        "Enabled": true,
        "InboundVerification": "shared_secret",
        "BotToken": "YOUR_BOT_TOKEN",
        "BotPublicName": "Your Login Bot",
        "UpdateMode": "Webhook",
        "WebhookBaseUrl": "https://auth.your-domain.com",
        "WebhookSecretToken": "YOUR_RANDOM_SECRET"
      }
    }
  }
}
```

Как и с Telegram, Veriqa регистрирует вебхук на старте в режиме `Webhook`. Эндпоинт —
`POST {WebhookBaseUrl}/api/channels/max/webhook`, аутентифицируется заголовком `X-Max-Bot-Api-Secret`.
`BotPublicName` — имя, показываемое пользователю на странице входа.

## Email

Email — единственный канал с двумя независимыми направлениями, и они могут работать вместе:

* **Pull (magic link)** — Veriqa отправляет письмо со ссылкой, пользователь по ней кликает. Включено
  по умолчанию (`PullEnabled`).
* **Push — письмо в один тап (one-tap email)** — пользователь отправляет готовое письмо на
  входящий адрес (один тап с кнопки или по QR), Veriqa его читает. Выключено по умолчанию
  (`PushEnabled`); требует входящей доставки и политики верификации отправителя. `Push` — значение
  в конфигурации; «письмо в один тап» — как режим называется для пользователей.

```json appsettings.json theme={null}
{
  "Veriqa": {
    "Channels": {
      "Email": {
        "Enabled": true,
        "InboundVerification": "shared_secret",
        "PublicBaseUrl": "https://auth.your-domain.com",
        "PreferredMode": "Pull",
        "PullEnabled": true,
        "PushEnabled": false,
        "TokenTtl": "00:05:00",
        "Outbound": {
          "Provider": "Smtp",
          "FromAddress": "login@your-domain.com",
          "FromName": "Veriqa",
          "Smtp": {
            "Host": "smtp.your-provider.com",
            "Port": 587,
            "UseSsl": false,
            "Username": "…",
            "Password": "…"
          }
        }
      }
    }
  }
}
```

`PublicBaseUrl` **обязателен** всегда, когда канал включён — из него строятся magic-link и QR-код.
`Port: 587` с `UseSsl: false` означает STARTTLS; `UseSsl: true` означает неявный TLS на порту 465.

`RequireStartTls` (по умолчанию `true`) действует, когда `UseSsl` равен `false` и заданы `Username`
и `Password`: если сервер не предлагает STARTTLS, соединение отвергается — пароль никогда не уходит
открытым текстом. Ставьте `false` только для внутреннего релея, к которому вы сознательно ходите без
TLS: тогда STARTTLS применяется, лишь если сервер его предложил. На анонимный релей (без учётных
данных) настройка не влияет.

`Username` и `Password` задаются **только вместе**. Обе настройки пустые — легальная конфигурация:
это релей без аутентификации, типовой вариант для внутреннего SMTP в контуре компании. А вот
заданное **ровно одно** из двух приложение отвергает на старте: аутентификация в таком случае не
выполнялась бы вовсе, то есть указанное значение молча никогда не применялось бы. Если Veriqa
не стартует с сообщением про `Username` и `Password` — удалите лишнее из двух значений либо задайте
оба.

### Какой режим выбрать

Magic link включён по умолчанию и не требует ничего, кроме исходящей почты, — это более дешёвый
старт. Письмо в один тап стоит входящего провайдера, секрета вебхука и политики отправителя — и
даёт четыре вещи, которых magic link дать не может:

* **Пользователю ничего не нужно доставлять.** Письмо идёт в обратную сторону, поэтому вход
  больше не зависит от того, дойдёт ли ваше исходящее письмо: ни папки «Спам», ни greylisting, ни
  репутации домена. При `PullEnabled: false` каналу вообще не нужен SMTP — валидатор настроек
  требует блок исходящей почты только при включённом Pull.
* **Нет ссылки, которую сожжёт почтовый шлюз.** Корпоративная почтовая защита открывает ссылки
  во входящих письмах для проверки, и одноразовый magic link может быть израсходован до того, как
  по нему кликнет человек. В письме, которое отправляет сам пользователь, такой ссылки нет.
* **Доказывается владение ящиком, а не факт получения письма.** Переход по ссылке показывает, что
  письмо до кого-то дошло, — в том числе через правило пересылки или общий ящик. Отправка письма
  показывает, что отправитель управляет ящиком, и отправитель проверяется политикой
  `VerificationPolicy` (по умолчанию `DmarcAlignedPass`).
* **Это то же действие, что и в мессенджерах.** Пользователь отправляет готовое сообщение в один
  тап — [секретное сообщение](/docs/ru/guides/supported-channels#секретное-сообщение). Email — тот же
  паттерн, только письмом вместо сообщения в чате: страница входа учит одной привычке, а не двум.

Режимы независимы и могут работать вместе: `PreferredMode` решает, какой из них страница входа
предлагает первым, а на compose-странице остаётся ручной фолбэк на magic link.

### Режим Push — письмо в один тап

Push дополнительно требует блок `Inbound`:

```json appsettings.json theme={null}
{
  "Inbound": {
    "Provider": "Webhook",
    "InboundAddress": "login@your-domain.com",
    "UsePlusAddressing": true,
    "VerificationPolicy": "DmarcAlignedPass",
    "WebhookSecretToken": "YOUR_SECRET"
  }
}
```

Ваш входящий провайдер (Mailgun, Postmark, SendGrid inbound parse, …) постит входящую почту на
`POST https://auth.your-domain.com/api/channels/email/inbound`, аутентифицируясь заголовком
`X-Email-Webhook-Secret`.

В мультитенантной установке тот же маршрут принимает и необязательный тенант-сегмент —
`POST https://auth.your-domain.com/api/channels/email/inbound/{tenant}`, — и каждый тенант
прописывает свой сегмент в настройках своего входящего провайдера. Письмо, пришедшее туда,
обрабатывается настройками `Inbound` и webhook-секретом **этого** тенанта. Без сегмента маршрут
использует настройки установки целиком.

`UsePlusAddressing` встраивает correlation-токен в reply-адрес (`login+{token}@…`), чем входящее
письмо и привязывается обратно к своей транзакции входа.

`VerificationPolicy` решает, насколько доверять отправителю — `From` в письме тривиально подделать,
поэтому это средство безопасности, а не формальность:

| Значение | Поведение |
| - | - |
| `DmarcAlignedPass` | По умолчанию. Требует DMARC-aligned pass — рекомендуемая настройка |
| `SpfOrDkimPass` | Слабее: достаточно прохождения SPF **или** DKIM |
| `AllowListOnly` | Только домены, перечисленные в `AllowedDomains` |

<Warning>
  Не отключайте верификацию отправителя в продакшне. Без неё любой, кто может подделать заголовок
  `From`, способен завершить чужой вход.
</Warning>

### Провайдеры

Поставляемые провайдеры — **`Smtp`** для исходящих и **`Webhook`** для входящих. Обе настройки также
принимают **`Custom`**, в этом случае вы регистрируете свой `IEmailOutboundSender` /
`IEmailInboundProcessor` в DI до вызова `AddEmail()`. Любое другое значение падает fail-fast на
старте, а не молча бездействует.

Свой `IEmailOutboundSender` отправляет только письмо входа, если он не отвечает
`SupportsClaimCompletionEmail` значением `true` и не реализует `SendClaimCompletionEmailAsync`: эти два
члена отправляют письма с кодом и со ссылкой, подтверждающие адрес почты при доборе claims. Без них
способы `Code` и `MagicLink` получения email недоступны, и веб-шаг предлагает только оставшиеся.

<Warning>
  **Оговорка про multi-instance.** Токены magic-link Email и Push-correlation токены держатся **в
  памяти** поставляемыми хранилищами. На нескольких инстансах ссылка, выданная одним инстансом,
  неизвестна остальным. Запускайте Email на одном инстансе, маршрутизируйте sticky-сессиями или
  зарегистрируйте распределённые реализации `IEmailActionTokenStore` и
  `IEmailPushCorrelationStore`. Остальных каналов это не касается.

  Своя реализация `IEmailPushCorrelationStore` должна переопределить `StoreOrReuseAsync` —
  идемпотентную регистрацию токена («у транзакции уже есть живой токен — верни его и ничего не
  пиши»). Реализация по умолчанию в интерфейсе просто пишет переданный токен, поэтому без
  переопределения прямой `mailto:`-режим (`Inbound.TokenInLocalPart = true`) выпускает **новый**
  correlation-токен на каждый рендер страницы входа, и все они живут до TTL или погашения. Захват
  индекса «транзакция → живой токен» обязан быть атомарным (compare-and-swap / `SET NX` /
  транзакция), а TTL переиспользованного токена — не продлеваться.
</Warning>

### Своё тело письма

Письмо входа — обычное **сообщение** механизма текстов: и тема, и HTML-документ тела целиком живут
значением ключа `Veriqa:MessageTemplates:sign-in-mail` (устройство ключа — в
[Справочнике конфигурации](/docs/ru/reference/configuration#veriqamessagetemplates)). Поэтому переписать
письмо можно **настройкой, не трогая код**: задайте свою лестницу вариантов на нужном уровне.

```json appsettings.json theme={null}
{
  "Veriqa": {
    "MessageTemplates": {
      "sign-in-mail": {
        "Templates": [
          {
            "Subject": "Sign in to {app}",
            "Html": "<!DOCTYPE html><html lang=\"{lang}\"><body><h1>Sign in to {app}</h1><p><a href=\"{link}\">{button}</a></p><p>{qr_instruction}</p><img src=\"cid:{qr_cid}\" alt=\"{qr_alt}\" width=\"220\" height=\"220\" /></body></html>",
            "Plain": "{text_intro}\n{open_link_hint}\n{link}\n\nRequest from {browser} on {os}, {region}"
          },
          {
            "Subject": "Sign in to {app}",
            "Html": "<!DOCTYPE html><html lang=\"{lang}\"><body><h1>Sign in to {app}</h1><p><a href=\"{link}\">{button}</a></p></body></html>",
            "Plain": "{text_intro}\n{open_link_hint}\n{link}"
          }
        ]
      }
    }
  }
}
```

Что здесь важно знать:

* **разметку в тексте варианта продукт не экранирует** — её пишет владелец конфигурации. Экранируются
  **значения слотов**: они приходят снаружи, в том числе от приложения по API;
* **видимые фразы остаются ключами локализации.** `{button}`, `{qr_instruction}`, `{qr_alt}` и прочие
  подобные — слоты локализованной строки: контракт сообщения указывает, на какой ключ смотрит слот, а
  подставится перевод на язык получателя. Разметка при этом одна на все языки, и переводить её копиями
  не нужно;
* **форму QR решает текст, а не настройка.** Вариант, сославшийся на `{qr_cid}`, получает
  PNG-вложение; вариант, сославшийся на `{qr_url}` (ссылку на QR-эндпоинт, если её кладёт ваш
  композер), получает картинку по ссылке и вложения не несёт; вариант, не назвавший ни одного из них,
  — письмо без QR. Лишнего вложения, на которое никто не ссылается, не бывает;
* **письмо одно, а редакций у ступени две.** Ступень лестницы — либо строка (текст, годный любому
  приёмнику), либо объект `{ "Subject": …, "Html": …, "Plain": … }`. Каждая редакция необязательна, но
  хотя бы одну ступень обязана заявить: ступень без единой редакции роняет старт с указанием рода
  сообщения и номера ступени. Ступень, заявившая только `Html`, для текстовой части не годится —
  подбор просто идёт ниже по лестнице, так что **две части одного письма могут прийти с разных
  ступеней**. Тема при этом берётся у ступени, выбранной для `Html`;
* **последняя ступень лестницы обязана опираться только на гарантированные слоты** (`app`,
  `valid_until`, `link`, `lang`). Детали инициатора и QR гарантированными не являются: транзакция без
  них штатно доезжает до ступени, которая их не называет, — и в письме не появляется ни пустого
  абзаца, ни литерала `{browser}`. Считается это **по каждой редакции отдельно**: подбор идёт среди
  ступеней, заявивших нужную редакцию, поэтому у лестницы две «последних» ступени — последняя с
  `Html` и последняя с `Plain`, — и требование распространяется на обе. Лестница, где последняя
  ступень с `Html` называет `{browser}`, а бесслотовой сделана только `Plain`, роняет старт;
* **письмо только в html (или только текстом) — законное объявление.** Если `Plain` не заявляет ни
  одна ступень, письмо уходит без текстовой части: ошибки в логе нет, подмены редакций нет. Ровно
  поэтому проверьте, что односоставное письмо — ваш выбор, а не забытая редакция: в данных эти два
  случая неотличимы.

<Warning>
  **Лестница — сеть под данными, а не замена настройке.** Она существует потому, что детали
  инициатора (`{browser}`, `{os}`, `{region}`) и QR в рантайме могут отсутствовать, — а не потому, что
  формулировки можно не дописывать. Объявление **заменяет лестницу целиком**: интегратор, объявивший
  одну ступень, чтобы «поменять формулировку», молча теряет всю поставляемую ядром лестницу вместе со
  всеми обеднёнными вариантами. И последняя ступень — не техническая заглушка: именно её текст
  пользователь прочитает чаще всего, когда контекста не будет.

  Оба этих случая ядро **говорит вслух один раз при старте** записью уровня `Warning`: когда
  объявленная лестница короче поставляемой по тому же адресу (в сообщении — род, адрес и оба числа
  ступеней) и когда `Plain` не заявляет ни одна ступень. Старт при этом не блокируется — короткая
  лестница и односоставное письмо остаются законными объявлениями. **Проверяется то, что видно при
  старте:** секция развёртывания и поставляемый файл, то есть уровень `Core`. Значения, объявленные
  тенантом, приложением или записью `ui_config`, при старте перечислить нельзя — их отсутствие в
  предупреждениях не означает, что там всё в порядке.
</Warning>

Если письмо нужно собрать **кодом** — взять разметку из своей CMS, подставить собственные значения
слотов или решать тему письма самостоятельно, — остаётся один шов: публичный
`IEmailMessageComposer`. Он получает данные отправки и возвращает готовое тело письма
(`EmailBodyContent`: тема, html-часть, текстовая часть и признак QR-вложения), а провайдер доставки
занимается только транспортом.

```csharp Program.cs theme={null}
builder.Services.AddSingleton<IEmailMessageComposer, CorporateEmailComposer>();
```

Регистрация подчиняется общему правилу подмены через DI: своя реализация, зарегистрированная до
`AddVeriqa*`, уже стоит в контейнере и поставляемая не добавляется; после — `RemoveAll` + `Add`.

Разметку письма задаёт текст варианта в конфигурации выше (её пример — в начале этого раздела); для
полного контроля — `IEmailMessageComposer`. Своя реализация композера берёт на себя **всё** тело
письма целиком: лестницу деградации, локализацию видимых фраз через Natural Key и
решение о QR-вложении она реализует сама.

### Нормализация адреса

Секция `Normalization` определяет, какую форму адреса Veriqa считает идентификатором пользователя:

```json appsettings.json theme={null}
{
  "Veriqa": {
    "Channels": {
      "Email": {
        "Normalization": {
          "LowercaseDomain": true,
          "LowercaseLocalPart": true,
          "ProviderSpecificCanonicalization": false
        }
      }
    }
  }
}
```

`ProviderSpecificCanonicalization` включает провайдер-специфичную канонизацию. Поддержано одно
семейство — Gmail (`gmail.com` и `googlemail.com`): точки в локальной части отбрасываются, `+`-тег
отсекается, `googlemail.com` приводится к `gmail.com`. Так `u.ser+shop@googlemail.com` и
`user@gmail.com` становятся одним пользователем. Домены вне этого списка не канонизируются никогда —
у многих провайдеров `+` является частью адреса, и общее правило склеило бы разных людей.

<Warning>
  **Включение на работающем деплое меняет identity.** Ранее входившие пользователи получат другой
  `sub`: адреса, которые считались разными, склеиваются в один. Миграцию ядро не выполняет — это
  операционное решение, ровно поэтому настройка выключена по умолчанию. Решайте её до первого входа
  пользователей, а не после.
</Warning>

## Как дать клиенту выбрать канал

Клиент может ограничить конкретный вход определёнными каналами через стандартный OIDC-параметр
`acr_values`, в форме `channel:{type}`:

```
GET /connect/authorize
  ?client_id=my-app
  &response_type=code
  &scope=openid profile
  &acr_values=channel:telegram
```

Перечислите несколько (`channel:telegram channel:whatsapp`), чтобы сузить выбор, не фиксируя его. Без
параметра предлагается каждый включённый канал.

## Чеклист

<Steps>
  <Step title="Адаптер зарегистрирован и включён">
    `AddXxx()` вызван **и** у секции стоит `"Enabled": true`.
  </Step>

  <Step title="Уровень проверки объявлен осознанно">
    У каждой канальной секции стоит `InboundVerification`, и значение соответствует режиму, в
    котором канал реально работает.
  </Step>

  <Step title="Вебхук доступен">
    `https://…/api/channels/…` публично доступен по HTTPS и не заблокирован вашим прокси.
  </Step>

  <Step title="Секреты в secret-store">
    Ни один токен или пароль не лежит в закоммиченном файле конфигурации.
  </Step>

  <Step title="Вход подтверждён от начала до конца">
    Реальное подтверждение с телефона завершает поток и выдаёт токены.
  </Step>
</Steps>

## Дальше

<CardGroup cols={2}>
  <Card title="Справочник конфигурации" icon="list" href="/docs/ru/reference/configuration">
    Каждая секция, ключ и дефолт в одном месте.
  </Card>

  <Card title="Конфигурация и кастомизация" icon="sliders" href="/docs/ru/guides/customization">
    Дизайн страницы входа, локализация, точки расширения.
  </Card>

  <Card title="Продакшн-харденинг" icon="shield-check" href="/docs/ru/guides/hardening">
    Что проверить перед выкаткой.
  </Card>

  <Card title="Архитектура" icon="cube" href="/docs/ru/concepts/architecture">
    Как адаптеры встроены в остальной коннектор.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.