Перейти к основному содержанию
Канал, которого нет в поставке, можно подключить отдельным сервисом на любом языке. HTTP-канал — адаптер из поставки Veriqa: он работает в хосте и передаёт вызовы по HTTP внешнему адаптеру. Внешний адаптер — сервис, который вы пишете сами и запускаете рядом с Veriqa или в своём периметре. Ни .NET, ни код в хосте не нужны: канал добавляется в конфигурации. Адаптер, который работает внутри .NET-хоста, описан на странице Свой адаптер на C#.
Wire-контракт между Veriqa и внешним адаптером — версии 1.0, он выходит в статусе preview. Пока контракт не объявлен стабильным, он ещё может измениться.

Модель транспорта

  • Платформа говорит с вашим адаптером, а не с Veriqa. Адаптер сам принимает вебхуки платформы или опрашивает её, проверяет события и сам отвечает на verification-handshake платформы. GET-маршрута для HTTP-канала Veriqa не открывает.
  • Адаптер отправляет события в Veriqa на маршруты, которые есть у каждого канала: /api/channels/{channelType}/webhook или /api/channels/{channelType}/webhook/{tenant}. Событие по транзакции уходит на маршрут тенанта этой транзакции — того, что пришёл в поле tenant вызовов deep-link и send-prompt этой транзакции; null — маршрут без сегмента тенанта. Второй раз Veriqa тенанта не сообщит: связь «транзакция → тенант» храните у себя.
  • Свой публичный вход адаптер защищает сам. Rate-limit Veriqa охраняет её собственные маршруты; вход, который адаптер открывает платформе, нуждается в своей защите от мусорного трафика.
  • Veriqa вызывает адаптер по одному адресу, Http:BaseUrl, — за всем, что ей нужно от канала.
  • Учётные данные платформы остаются у адаптера. Veriqa их не запрашивает и не хранит.
  • Одна регистрация — один внешний адаптер: один адрес, один секрет транспорта, одни факты канала. Без Http:Tenant она обслуживает всех тенантов установки. Тенант со своей реализацией получает свою регистрацию под своим именем (например shop-a-zalo), и Http:Tenant называет этого тенанта.
  • Стандартный образ Veriqa регистрирует HTTP-каналы всегда: канал добавляется переменными окружения и перезапуском контейнера, без пересборки образа. Embedded-хост подключает пакет Veriqa.Core.ChannelAdapter.Http и добавляет один вызов:
Program.cs

Конфигурация

Каждая секция Veriqa:Channels:{channelType} с подсекцией Http становится одним HTTP-каналом под именем секции:
appsettings.json
Тот же канал переменными окружения, вместе с секретом транспорта — держите его там или в хранилище секретов, а не в appsettings.json. Тип канала с дефисом из shell не экспортировать, но environment: compose-файла и docker run -e его принимают:
Что важно знать о ключах (полный перечень — в справочнике конфигурации):
  • Факты канала и кнопку задаёт оператор — ключами Http:Capabilities:* (по ключу на свойство ChannelCapabilities), Http:DisplayName и Http:IconSvgPath. Сетевого манифеста нет: Veriqa не спрашивает адаптер, что он умеет. Незаданный факт получает безопасное умолчание ChannelCapabilities — все булевы false, единственный вид контента plain_text, своего правила об эмодзи у канала нет (RendersEmoji не задан), номера телефона канал не даёт (Номер телефона).
  • Http:Tenant называет тенанта-владельца регистрации. Она обслуживает только его; у остальных тенантов этого канала нет — ни на входящем маршруте, ни в событиях, ни в исходящих вызовах. Ключ не задан — регистрация обслуживает всех тенантов установки.
  • Http:AdapterSettings — непрозрачная строка до 8192 символов, которую Veriqa пересылает дословно в каждом вызове и никогда не читает. У неё уровни ядра и тенанта; значение тенанта заменяет значение ядра целиком.
  • Http:AllowInsecureHttp разрешает базовый URL со схемой http. Только для изолированной внутренней сети (соседний контейнер); при старте Veriqa пишет предупреждение.
  • Все значения Http:* фиксируются на старте, кроме Http:AdapterSettings, который резолвится на каждый вызов. Остальные меняются только перезапуском.
  • Имя канала — в формате из раздела Имя канала, а имена каналов поставки Veriqa зарезервированы, даже если встроенный канал в хост не подключён: max, telegram, whatsapp, email, instagram, messenger, wechat, viber. Резерв растёт с выходом новых встроенных каналов.
  • Ошибка конфигурации, найденная на старте, останавливает его с сообщением, называющим ключ или секцию канала, — например: базовый URL не задан или не абсолютный, базовый URL со схемой http без Http:AllowInsecureHttp, не задан Http:TransportSecret, секрет не в форме whsec_, один секрет у двух регистраций, запрещённая схема deep link, таймаут не в форме hh:mm:ss, Http:AdapterSettings длиннее 8192 символов, зарезервированное имя или имя, которое уже занято другим каналом хоста. Пустой Http:Tenant — тоже ошибка: чтобы обслуживать всех тенантов, ключ удаляют.
В отличие от канала через AddChannel (замечание в §2), у HTTP-канала InboundVerification проверяется на старте: регистрация объявляет канал ядру, и включённый HTTP-канал без значения не даёт хосту стартовать.

Номер телефона

Канал, который может дать номер телефона пользователя, объявляет это в Http:Capabilities:PhoneNumber — тот же факт, что встроенный канал объявляет в коде (см. Телефон по запросу и Номер телефона). Без объявления канал номера не даёт и не предлагается для входа, которому номер обязателен.
appsettings.json
Регистр имён не различается. Старт останавливают пять ошибок конфигурации: PhoneNumber одним значением вместо секции, значение вне названных имён, Acquisition одним значением вместо списка, Proof: Proven без Acquisition и OnRequest без phone_number_request в SupportedMessageKinds — запрос не дошёл бы до адаптера. Proof — потолок того, что могут утверждать ваши события. identity.phone_number_verified: true события принимается только у регистрации, объявившей Proven; у любой другой Veriqa понижает его до false и пишет предупреждение. false и отсутствие поля не повышаются: объявление Proven не делает доказанным номер, о котором адаптер промолчал.

Старт по коду

Канал, чья ссылка не может нести транзакцию или чья платформа доставляет её не всегда, объявляет старт по коду в Http:Capabilities:CodeStart — теми же двумя членами, что встроенный канал задаёт в коде. Ядро показывает код на своих страницах, пользователь отправляет его вашему боту, а адаптер отправляет его событием auth_start_by_code (см. Wire-контракт).
appsettings.json
Http:UnmatchedCodeReplyText — ответ на код, не называющий живого входа; не задан — отвечает Http:StaleLinkReplyText. Шаблон проверяется там, где канал показывается, как у встроенного канала, а не на старте. Старт останавливают три ошибки конфигурации: CodeStart одним значением вместо секции, LinkCarriesTransaction не true и не false, MessageTemplate без LinkCarriesTransaction. При LinkCarriesTransaction: true блок запасного кода и его настройка SpareStartCodeEnabled работают у HTTP-канала, как у любого канала. auth_start_by_code от регистрации, не объявившей старт, не обрабатывается.
Пока старт по коду доступен только регистрации без владельца. Регистрация, которая его объявила и задала Http:Tenant, останавливает хост на старте с сообщением, называющим оба ключа.

Wire-контракт

Каждый вызов Veriqa — подписанный POST JSON-конверта на Http:BaseUrl: version, operation, channel_type (один вход может обслуживать несколько регистраций), tenant (или null), adapter_settings (или null) и payload. Ответ — 200 с конвертом из version и ровно одного из двух: result или error (code и message). Поля каждого payload, результата и события — в машиночитаемой JSON-схеме провода; это руководство их не повторяет. Правила, которых схема не выражает:
  • Коды ошибок. Коды, которые называет провод, — в схеме, и любой другой код тоже принимается. Код error пишется в лог и больше ничего не решает, за одним исключением: channel_cannot_continue в ответе на send-prompt сразу завершает транзакцию отказом. Любой другой error действует по своей операции, и после него вызов не повторяется:
    • send-prompt — транзакция остаётся открытой и ждёт нового auth_start пользователя или истечения срока;
    • deep-link — у пользователя нет входа в ваш канал: страница входа рисуется без него;
    • send-message, report-outcome — отказ пишется в лог и учитывается в метриках, и только.
  • deep-link идемпотентен: повторный вызов по той же транзакции возвращает тот же url и не создаёт нового состояния.
  • У ответа есть границы. Ответ читается не более 1 МБ и сверх лимита отвергается, редиректы не выполняются, deep link принимается только абсолютным URI со схемой https или из Http:DeepLinkSchemes, не длиннее 2048 символов.
  • Таймауты и повторы. Попытка ждёт Http:OperationTimeout (по умолчанию 10 с); deep-link — Http:DeepLinkTimeout (2 с), потому что стоит на пути страницы входа. send-prompt, send-message и report-outcome повторяются при транспортном отказе — нет соединения, таймаут, 5xx, 429 — до трёх раз, все попытки в пределах Http:RetryBudget (20 с). Больше не повторяется ничего: 200 с error, 3xx, прочие 4xx, слишком большой или некорректный ответ. deep-link и health — одна попытка.
  • Второе действие запроса номера возвращается одним из двух способов — тем, который позволяет платформа: событием phone_step_action либо событием unaddressed, чей text равен подписи второй кнопки. Незнакомый Veriqa action делает событие unrelated. На саму кнопку «поделиться» отвечает phone_shared.
  • Номер телефона в идентичности. identity.phone_number любого события принимается как номер пользователя — это способ «автоматически», — а identity.phone_number_verified: true — только в пределах Proof, объявленного регистрацией (см. Номер телефона).
  • auth_start_by_code уходит на тот маршрут, что и прочие события вашего бота. Транзакции оно не называет, поэтому правило тенанта транзакции к нему неприменимо: Veriqa ищет код по всей установке, на какой бы маршрут ни пришло событие, и берёт тенанта с найденной транзакции.
  • Отвергайте конверт события на входе вызовов. Тело с event вместо operation, пришедшее на Http:BaseUrl, — ваше же событие, отражённое обратно; ответ на него — что угодно, кроме 200 с result.
  • Обслуживайте вызовы, пока открыт свой запрос. На auth_start Veriqa шлёт адаптеру send-prompt до того, как ответит на ваш POST события. Адаптер, который не умеет принимать вызовы параллельно со своими запросами в Veriqa, получает таймаут вопроса, и транзакция ждёт истечения срока.

Подпись

Оба направления подписываются секретом транспорта регистрации по схеме Standard Webhooks v1 (HMAC-SHA256) — библиотека для неё есть в любом языке. Заголовки — webhook-id, webhook-timestamp и webhook-signature; метка времени принимается в пределах 5 минут в обе стороны от часов получателя.
  • Ротация без простоя. Новый секрет — в Http:TransportSecret, старый — в Http:PreviousTransportSecret: отправитель ставит обе подписи, получатель принимает любую. Пока предыдущий секрет задан, Veriqa пишет при старте предупреждение.
  • Ответы не подписываются: их подлинность держится на соединении, которое Veriqa открыла по настроенному адресу, — поэтому по умолчанию требуется https.
  • Неверная подпись — 403, без конверта и без какого-либо эффекта, в обе стороны: так Veriqa отвечает на ваше событие без подписи или с неверной подписью, и так же обязан отвечать на такой вызов ваш адаптер.
  • У каждой регистрации свой секрет. Общий секрет позволил бы одной регистрации выдать себя за другую, поэтому хост не стартует.

Идемпотентность

  • webhook-id вызовов send-prompt, send-message и report-outcome — ключ идемпотентности. Повтор с ключом, который вы уже успешно обслужили, ничего нового не отправляет и возвращает результат первого вызова — ту же ссылку на сообщение. У deep-link и health webhook-id уникален, но ключом не является.
  • webhook-id события выбираете вы: уникальный на событие и неизменный при повторной доставке того же события. Из него Veriqa строит ключи вызванных событием send-prompt и report-outcome, поэтому повторно доставленное событие не задаёт вопрос дважды.
  • Неуспешная попытка ключ не связывает: следующая попытка с ним обслуживается заново.
  • Срок хранения выбирает оператор адаптера, с двумя нижними границами: не меньше бюджета повторов Veriqa (Http:RetryBudget), а для ключа, рождённого событием, — ещё и не меньше окна, в котором сам адаптер может доставить это событие повторно.

Версия провода и статус preview

Каждый конверт в обе стороны несёт version — версию провода отправителя, major.minor; первая — 1.0, в статусе preview. В пределах мажорной версии изменения только аддитивны — новое необязательное поле, операция, значение перечисления или тип события, — и у каждого объявлено поведение получателя, который его не знает: незнакомое поле игнорируется, на незнакомую операцию — ответ unsupported_operation, незнакомый вариант вопроса показывается как вопрос без деталей, незнакомое событие Veriqa считает unrelated. Удаление или переименование поля, смена его смысла — только новой мажорной версией. На вызов другой мажорной версии отвечайте unsupported_wire_version; Veriqa, в свою очередь, событие другой мажорной версии не обрабатывает, а такой ответ считает ответом вне контракта. Каждое событие несёт и adapter_version — версию вашего адаптера в semver.

Conformance-набор

Veriqa поставляет исполняемый conformance-набор, который проверяет адаптер на соответствие проводу, на каком бы языке тот ни был написан. Он проверяет три вещи:
  • вызовы через настоящую обёртку — каждую операцию, повтор с тем же ключом, неверный секрет и метку времени вне окна. Эти вызовы делает тот самый код HTTP-канала, что работает в Veriqa, поэтому набор не может разойтись с Veriqa;
  • некорректные вызовы, которых Veriqa не делает, — без подписи, с незнакомыми полем, вариантом или операцией, чужой мажорной версии, конверт события, отражённый на вход вызовов, — их шлёт отдельный отправитель;
  • приёмник событий — он играет Veriqa для ваших событий: проверяет подпись и схему каждого, вызывает send-prompt до ответа на auth_start и проверяет, что каждое событие пришло на маршрут тенанта своей транзакции.
Номер телефона проверяют два сценария, и каждый входит в прогон, только если регистрация объявила его способ: send-message.phone-number-request — при OnRequest, identity.phone-automatic — при Automatic. Сценария нет в отчёте — значит, его способ не объявлен. Событие с phone_number_verified: true без объявленного Proof: Proven проваливает events.all-valid. Регистрация, объявившая старт по коду, получает сценарий flow.code-start: набор придумывает код, просит отправить его боту из deep link — заглушку платформы запросом send-text или вас строкой ACTION — и ждёт auth_start_by_code с этими цифрами от пользователя прогона. При LinkCarriesTransaction: false сценарии, начинающие транзакцию по её ссылке, — flow.confirm, flow.decline, identity.phone-automatic — в прогон не входят: ссылка, которая только открывает чат, ничего не начинает. Подтверждение после старта кодом набор тогда не проигрывает — за придуманным кодом транзакции нет, — поэтому этот путь проверяйте живым прогоном. Набор — контейнерный образ, собирается из корня репозитория:
Запускайте его рядом с адаптером, с конфигурацией канала теми же ключами Veriqa__Channels__<type>__*, что читает Veriqa, и собственными ключами под Conformance__*: обязательны ChannelType и ChannelUserId; Mode, Tenants и ReportPath — по желанию. В interactive и automatic набор стоит на месте Veriqa: слушает порт 8080 образа на тех же путях, /api/channels/{type}/webhook и /api/channels/{type}/webhook/{tenant}, поэтому адаптер меняет только базовый адрес Veriqa, на который отправляет события. Сколько ждать события — Conformance__EventTimeoutSeconds. Набор печатает по строке на сценарий — PASS <id>, FAIL <id>: <причина> или N/A <id>: <причина>, — пишет JSON-отчёт в Conformance__ReportPath и завершается кодом 0 (ни один сценарий не упал), 1 (упал хотя бы один) или 2 (прогон провести не удалось; сообщение называет, что исправить). Полный перечень сценариев — в README src/core/Veriqa.Core.ChannelAdapter.Http.Conformance. Машиночитаемая схема провода (JSON Schema) поставляется вместе с набором: в образе — /schema/http-channel-wire-v1.schema.json, в репозитории — src/core/Veriqa.Core.ChannelAdapter.Http.Conformance/Schema/.

Референсные адаптеры

Три внешних адаптера в репозитории показывают весь контракт на настоящей платформе:
  • samples/python/custom-channel-telegram/ — Telegram на Python;
  • samples/node/custom-channel-whatsapp-twilio/ — WhatsApp через Twilio на Node;
  • samples/node/custom-channel-whatsapp-baileys/ — WhatsApp через Baileys на Node — рабочий адаптер для отладки, небольших команд и собственного использования; номер при этом могут заблокировать.
Каждый запускается рядом с Veriqa из своей папки:
Вместе они показывают все операции и все события своего канала, подпись в обе стороны, идемпотентность (повтор не шлёт второго сообщения), учётные данные по тенанту, adapter_settings как собственную настройку адаптера и защиту собственного публичного входа адаптера. Адаптер Twilio вдобавок отвечает channel_cannot_continue, когда платформа не может доставить вопрос; адаптер Telegram делит обновления (update) между Veriqa и вашим собственным ботом. Два из них показывают оба способа получить номер телефона: адаптер Telegram — по запросу (показывает запрос клавиатурой с кнопкой контакта и возвращает вторую кнопку текстом события unaddressed); адаптер Twilio — автоматически (номер отправителя едет в идентичности каждого события). Адаптер Telegram показывает и старт по коду — за переключателем CODE_START: off по умолчанию, fallback — для регистрации с LinkCarriesTransaction: true, only — с false; его CI прогоняет набор второй раз в режиме only. Адаптер Baileys рабочий и предназначен для отладки сценариев с WhatsApp, небольших команд и собственного использования: Baileys — неофициальный клиент WhatsApp Web на вашем собственном номере, и WhatsApp может этот номер заблокировать. Вопрос он задаёт опросом, а сообщения забирает своей исходящей сессией. README каждого описывает его конфигурацию, значение InboundVerification и ручной живой прогон. Это образцы, чтобы разобраться и скопировать, а не каталог адаптеров сообщества.

InboundVerification внешнего адаптера

Правило раздела Объявите уровень проверки своего канала — и задокументируйте его действует для внешнего адаптера без изменений: трафик платформы проверяет ваш адаптер, поэтому значение для каждого режима приёма своего канала называете вы в своей документации, а оператор объявляет его в секции канала. Умолчания нет. Подпись транспорта между адаптером и Veriqa проверяется всегда, и это значение её не описывает. Референсные адаптеры называют свои: В той же документации перечислите ключи верхнего уровня своего raw_metadata, несущие персональные данные, включая нативный ID пользователя платформы: по этому перечню интегратор решает, что ему можно хранить.