Veriqa:Channels:{Channel} и сделать его вебхук доступным из интернета.
Program.cs
Каждый канал — свой пакет. Базовый набор ставится одним метапакетом
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*() вызван — это ошибка компиляции,
а не молчаливо отсутствующий канал."Enabled": true — иначе регистрируются лишь его опции и стартовый валидатор, а канал
никогда не появляется на странице входа. Добавляйте только те каналы, которые используете.
Уровень проверки входящего события
У каждой канальной секции есть обязательный ключInboundVerification — он объявляет, как
проверяется подлинность входящего события этого канала. Факт объявляете вы: Veriqa видит лишь то,
что проверка выполнилась и прошла, но не то, чем именно она была.
Значения для поставляемых каналов — по режиму, в котором вы их запускаете:
Канал в режиме
Polling, которому объявили что-то кроме outbound_fetch, даёт Warning при
старте — старт при этом продолжается. Полный словарь значений — в
справочнике конфигурации.
Telegram
Что нужно: бот, созданный через @BotFather, и его токен.appsettings.json
В режиме
Webhook Veriqa регистрирует вебхук сам на старте (и снимает при остановке) — вы не
вызываете setWebhook вручную. Эндпоинт — POST {WebhookBaseUrl}/api/channels/telegram/webhook, и
каждый запрос аутентифицируется заголовком X-Telegram-Bot-Api-Secret-Token против
WebhookSecretToken.
appsettings.json
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 означает, что валидные доставки отвергаются.
С каналом поставляются два провайдера доставки: Meta Cloud API (
MetaCloudApi, по умолчанию) и
Twilio (Twilio). Ключ Provider выбирает, какой из них регистрируется, и объявляет, какого
провайдера ожидает установка; на старте он сверяется с фактически зарегистрированным. Собственный
провайдер, подключённый через UseWhatsAppProvider<TProvider>(), побеждает поставляемый, и в
Provider ставится его код. Расхождение останавливает хост с ошибкой, называющей оба кода.Доставка через Twilio
Что нужно: аккаунт Twilio (Account SID и Auth Token), WhatsApp-отправитель — Twilio Sandbox for WhatsApp, чтобы попробовать, и зарегистрированный отправитель в production, — и контентtwilio/quick-reply, созданный в Twilio Content Template Builder.
appsettings.json
BusinessPhoneNumber — номер отправителя Twilio (при Sandbox — номер песочницы). При
Provider = Twilio обязательны AccountSid, AuthToken, QuickReplyContentSid и WebhookUrl:
без любого из них хост не стартует, а ошибка называет ключ, но не его значение. ApiBaseUrl
необязателен, по умолчанию https://api.twilio.com. Тенант хранит креды Twilio в той же группе кредов
WhatsApp, что и креды Meta.
Контент quick-reply. Вопрос подтверждения уходит одним контентом с двумя кнопками. Создайте его
типа twilio/quick-reply ровно с этими переменными — Veriqa заполняет их сама:
twilio/quick-reply
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-адаптер канала на любом языке.
Текст предзаполненного сообщения
WhatsApp — единственный из поставляемых каналов, где deep link несёт текст сообщения, а не служебный параметр: пользователь видит его в поле ввода и отправляет как обычное сообщение. Из коробки это короткое многострочное сообщение с кодом входа на последней строке:Базовый текст (en)
wwwroot/locales/{язык}.json, см. Локализация). Ключей два —
полный вариант с именем приложения и запасной без него; второй используется, когда имя приложения
недоступно. Отдельной секции в Veriqa:Channels:WhatsApp для текста нет.
wwwroot/locales/ru.json
en.json). Доступны два
слота: {app} — имя приложения, к которому идёт вход, и {code} — код входа auth_…. Язык
берётся от страницы входа, поэтому переводить стоит все языки, которые вы поставляете.
Имя приложения приходит из регистрации вашего OIDC-клиента и подставляется как есть —
Veriqa его не сокращает (общий предел значения слота — 128 символов). Держите имя коротким:
длинное ломает вёрстку сообщения и целиком уходит в QR-код, увеличивая его плотность
(см. предупреждение ниже).
Отсюда правила оформления, которые стоит соблюдать:
- код — последней отдельной строкой: он длиной 48 символов (
auth_+ идентификатор транзакции на 43 символа) и не влезает ни в какую рамку; - не использовать
*,_,~как элементы графики — WhatsApp трактует их как разметку (жирный, курсив, зачёркнутый). Безопасны-,.,:,+,|; - рамки с вертикальными палками (
|…|) выравниваются только внутри моноширинного блока (тройные бэктики) — в обычном сообщении шрифт пропорциональный и рамка разъедется. Часть клиентов к тому же показывает в поле ввода сами бэктики, а моноширинность — уже в отправленном сообщении, поэтому такое оформление проверяйте на устройстве до выкладки; - горизонтальные линейки (
------) работают в любом клиенте и обёртки не требуют.
Русские варианты без графики и с линейками-заголовком стоят ровно на пороге — запаса у них
нет: любое удлинение текста поднимает версию QR и выводит код из нормы. Английский вариант по
умолчанию проходит с запасом. У вариантов с рамкой гейтов два: русские рамки не берут ещё и порог
плотности (2.37 и 1.88), а моноширинный блок нужен любой рамке — даже английская, стоящая ровно на
пороге, в обычном сообщении разъедется. Отсюда «Нет» в колонке.
Практический вывод: если сообщение показывается и как QR-код, держитесь варианта без графики
или ограничьтесь горизонтальными линейками — рамки и псевдографика делают QR-код трудным для
камеры. Русский текст обходится примерно вдвое дороже английского при том же оформлении: тот же
текст по умолчанию стоит 388 символов против 171.
Готовые образцы каждого варианта — ниже;
{app} и {code} подставит Veriqa.
Без графики — вариант по умолчанию
Линейки из дефисов + центрированный заголовок
Линейки из дефисов
Рамка «+--+» — только после проверки на устройстве
Рамка Unicode — самый дорогой вариант, для QR непригоден
\n:
wwwroot/locales/ru.json — вариант с линейками
Оформляйте оба ключа сразу. Второй — запасной вариант без имени приложения; если оформить
только первый, пользователи, у которых имя приложения недоступно, увидят другое сообщение.
Выравнивание по
{app} — приблизительное: имя приложения у каждого клиента своей длины, поэтому
ни центрирование пробелами, ни правая граница рамки не сойдутся точно. Если рамка нужна ровной —
берите вариант без имени приложения.MAX
Что нужно: бот MAX и его токен. Конфигурация зеркалит Telegram:appsettings.json
Webhook. Эндпоинт —
POST {WebhookBaseUrl}/api/channels/max/webhook, аутентифицируется заголовком X-Max-Bot-Api-Secret.
BotPublicName — имя, показываемое пользователю на странице входа.
- Pull (magic link) — Veriqa отправляет письмо со ссылкой, пользователь по ней кликает. Включено
по умолчанию (
PullEnabled). - Push — письмо в один тап (one-tap email) — пользователь отправляет готовое письмо на
входящий адрес (один тап с кнопки или по QR), Veriqa его читает. Выключено по умолчанию
(
PushEnabled); требует входящей доставки и политики верификации отправителя.Push— значение в конфигурации; «письмо в один тап» — как режим называется для пользователей.
appsettings.json
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). - Это то же действие, что и в мессенджерах. Пользователь отправляет готовое сообщение в один тап — секретное сообщение. Email — тот же паттерн, только письмом вместо сообщения в чате: страница входа учит одной привычке, а не двум.
PreferredMode решает, какой из них страница входа
предлагает первым, а на compose-странице остаётся ручной фолбэк на magic link.
Режим Push — письмо в один тап
Push дополнительно требует блокInbound:
appsettings.json
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 в письме тривиально подделать,
поэтому это средство безопасности, а не формальность:
Провайдеры
Поставляемые провайдеры —Smtp для исходящих и Webhook для входящих. Обе настройки также
принимают Custom, в этом случае вы регистрируете свой IEmailOutboundSender /
IEmailInboundProcessor в DI до вызова AddEmail(). Любое другое значение падает fail-fast на
старте, а не молча бездействует.
Свой IEmailOutboundSender отправляет только письмо входа, если он не отвечает
SupportsClaimCompletionEmail значением true и не реализует SendClaimCompletionEmailAsync: эти два
члена отправляют письма с кодом и со ссылкой, подтверждающие адрес почты при доборе claims. Без них
способы Code и MagicLink получения email недоступны, и веб-шаг предлагает только оставшиеся.
Своё тело письма
Письмо входа — обычное сообщение механизма текстов: и тема, и HTML-документ тела целиком живут значением ключаVeriqa:MessageTemplates:sign-in-mail (устройство ключа — в
Справочнике конфигурации). Поэтому переписать
письмо можно настройкой, не трогая код: задайте свою лестницу вариантов на нужном уровне.
appsettings.json
- разметку в тексте варианта продукт не экранирует — её пишет владелец конфигурации. Экранируются значения слотов: они приходят снаружи, в том числе от приложения по 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не заявляет ни одна ступень, письмо уходит без текстовой части: ошибки в логе нет, подмены редакций нет. Ровно поэтому проверьте, что односоставное письмо — ваш выбор, а не забытая редакция: в данных эти два случая неотличимы.
IEmailMessageComposer. Он получает данные отправки и возвращает готовое тело письма
(EmailBodyContent: тема, html-часть, текстовая часть и признак QR-вложения), а провайдер доставки
занимается только транспортом.
Program.cs
AddVeriqa*, уже стоит в контейнере и поставляемая не добавляется; после — RemoveAll + Add.
Разметку письма задаёт текст варианта в конфигурации выше (её пример — в начале этого раздела); для
полного контроля — IEmailMessageComposer. Своя реализация композера берёт на себя всё тело
письма целиком: лестницу деградации, локализацию видимых фраз через Natural Key и
решение о QR-вложении она реализует сама.
Нормализация адреса
СекцияNormalization определяет, какую форму адреса Veriqa считает идентификатором пользователя:
appsettings.json
ProviderSpecificCanonicalization включает провайдер-специфичную канонизацию. Поддержано одно
семейство — Gmail (gmail.com и googlemail.com): точки в локальной части отбрасываются, +-тег
отсекается, googlemail.com приводится к gmail.com. Так u.ser+shop@googlemail.com и
user@gmail.com становятся одним пользователем. Домены вне этого списка не канонизируются никогда —
у многих провайдеров + является частью адреса, и общее правило склеило бы разных людей.
Как дать клиенту выбрать канал
Клиент может ограничить конкретный вход определёнными каналами через стандартный OIDC-параметрacr_values, в форме channel:{type}:
channel:telegram channel:whatsapp), чтобы сузить выбор, не фиксируя его. Без
параметра предлагается каждый включённый канал.
Чеклист
1
Адаптер зарегистрирован и включён
AddXxx() вызван и у секции стоит "Enabled": true.2
Уровень проверки объявлен осознанно
У каждой канальной секции стоит
InboundVerification, и значение соответствует режиму, в
котором канал реально работает.3
Вебхук доступен
https://…/api/channels/… публично доступен по HTTPS и не заблокирован вашим прокси.4
Секреты в secret-store
Ни один токен или пароль не лежит в закоммиченном файле конфигурации.
5
Вход подтверждён от начала до конца
Реальное подтверждение с телефона завершает поток и выдаёт токены.
Дальше
Справочник конфигурации
Каждая секция, ключ и дефолт в одном месте.
Конфигурация и кастомизация
Дизайн страницы входа, локализация, точки расширения.
Продакшн-харденинг
Что проверить перед выкаткой.
Архитектура
Как адаптеры встроены в остальной коннектор.