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равен подписи второй кнопки. Незнакомый Veriqaactionделает событиеunrelated. На саму кнопку «поделиться» отвечаетphone_shared. - Номер телефона в идентичности.
identity.phone_numberлюбого события принимается как номер пользователя — это способ «автоматически», — аidentity.phone_number_verified: true— только в пределахProof, объявленного регистрацией (см. Номер телефона). auth_start_by_codeуходит на тот маршрут, что и прочие события вашего бота. Транзакции оно не называет, поэтому правило тенанта транзакции к нему неприменимо: Veriqa ищет код по всей установке, на какой бы маршрут ни пришло событие, и берёт тенанта с найденной транзакции.- Отвергайте конверт события на входе вызовов. Тело с
eventвместоoperation, пришедшее наHttp:BaseUrl, — ваше же событие, отражённое обратно; ответ на него — что угодно, кроме200сresult. - Обслуживайте вызовы, пока открыт свой запрос. На
auth_startVeriqa шлёт адаптеруsend-promptдо того, как ответит на вашPOSTсобытия. Адаптер, который не умеет принимать вызовы параллельно со своими запросами в Veriqa, получает таймаут вопроса, и транзакция ждёт истечения срока.
Подпись
Оба направления подписываются секретом транспорта регистрации по схеме Standard Webhooksv1 (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иhealthwebhook-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 — рабочий адаптер для отладки, небольших команд и собственного использования; номер при этом могут заблокировать.
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 пользователя платформы: по этому перечню интегратор решает, что ему можно
хранить.