> ## 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.

# Свой адаптер на любом языке

> Подключите канал, которого нет в поставке, отдельным сервисом на любом языке: Veriqa вызывает его по HTTP, а канал добавляется в конфигурации без кода в хосте.

Канал, которого нет в поставке, можно подключить отдельным сервисом на любом языке. **HTTP-канал** —
адаптер из поставки Veriqa: он работает в хосте и передаёт вызовы по HTTP **внешнему адаптеру**.
Внешний адаптер — сервис, который вы пишете сами и запускаете рядом с Veriqa или в своём периметре.
Ни .NET, ни код в хосте не нужны: канал добавляется в конфигурации.

Адаптер, который работает внутри .NET-хоста, описан на странице
[Свой адаптер на C#](/docs/ru/guides/custom-channel-adapter).

<Note>
  Wire-контракт между Veriqa и внешним адаптером — версии `1.0`, он выходит в статусе **`preview`**.
  Пока контракт не объявлен стабильным, он ещё может измениться.
</Note>

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

* **Платформа говорит с вашим адаптером, а не с 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` и добавляет один вызов:

```csharp Program.cs theme={null}
authServer.AddChannelAdapters(adapters => adapters.AddHttpChannels());
```

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

Каждая секция `Veriqa:Channels:{channelType}` с подсекцией `Http` становится одним HTTP-каналом под
именем секции:

```json appsettings.json theme={null}
{
  "Veriqa": {
    "Channels": {
      "acme-chat": {
        "InboundVerification": "signature",
        "Http": {
          "BaseUrl": "https://acme-adapter.internal/",
          "DisplayName": "Acme Chat",
          "Capabilities": {
            "SupportsInChannelConfirmation": true,
            "DeliversOutcomeNotice": true
          }
        }
      }
    }
  }
}
```

Тот же канал переменными окружения, вместе с секретом транспорта — держите его там или в хранилище
секретов, а не в `appsettings.json`. Тип канала с дефисом из shell не экспортировать, но
`environment:` compose-файла и `docker run -e` его принимают:

```bash theme={null}
Veriqa__Channels__acme-chat__InboundVerification=signature
Veriqa__Channels__acme-chat__Http__BaseUrl=https://acme-adapter.internal/
Veriqa__Channels__acme-chat__Http__TransportSecret=whsec_<base64 от 24–64 случайных байт>
Veriqa__Channels__acme-chat__Http__Capabilities__SupportsInChannelConfirmation=true
```

Что важно знать о ключах (полный перечень — в
[справочнике конфигурации](/docs/ru/reference/configuration#ключи-в-каталоге-объявлений)):

* **Факты канала и кнопку задаёт оператор** — ключами `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`, который резолвится на
  каждый вызов. Остальные меняются только перезапуском.
* **Имя канала** — в формате из раздела [Имя канала](/docs/ru/guides/custom-channel-adapter#3-имя-канала), а имена каналов поставки 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` — тот же факт, что встроенный канал объявляет в коде (см.
[Телефон по запросу](/docs/ru/guides/custom-channel-adapter#телефон-по-запросу) и [Номер телефона](/docs/ru/guides/phone-number)).
Без объявления канал номера не даёт и не предлагается для входа, которому номер обязателен.

| Ключ (под `Http:Capabilities:PhoneNumber`) | Значения | Не задан |
| - | - | - |
| `Acquisition:{n}` | список: `Automatic` — номер приходит в идентичности ваших событий, ничего не запрашивается; `OnRequest` — Veriqa запрашивает его вызовом `send-message` вида `phone_number_request`. Можно указать оба | пусто — канал номера не даёт |
| `Proof` | `Proven` — номер из этого канала доказанно принадлежит пользователю; `NotProven` | `NotProven` |

```json appsettings.json theme={null}
"Capabilities": {
  "SupportedMessageKinds": [ "plain_text", "phone_number_request" ],
  "PhoneNumber": {
    "Acquisition": [ "OnRequest" ],
    "Proof": "Proven"
  }
}
```

```bash theme={null}
Veriqa__Channels__acme-chat__Http__Capabilities__SupportedMessageKinds__0=plain_text
Veriqa__Channels__acme-chat__Http__Capabilities__SupportedMessageKinds__1=phone_number_request
Veriqa__Channels__acme-chat__Http__Capabilities__PhoneNumber__Acquisition__0=OnRequest
Veriqa__Channels__acme-chat__Http__Capabilities__PhoneNumber__Proof=Proven
```

Регистр имён не различается. Старт останавливают пять ошибок конфигурации: `PhoneNumber` одним
значением вместо секции, значение вне названных имён, `Acquisition` одним значением вместо списка,
`Proof: Proven` без `Acquisition` и `OnRequest` без `phone_number_request` в `SupportedMessageKinds` —
запрос не дошёл бы до адаптера.

`Proof` — **потолок** того, что могут утверждать ваши события. `identity.phone_number_verified: true`
события принимается только у регистрации, объявившей `Proven`; у любой другой Veriqa понижает его до
`false` и пишет предупреждение. `false` и отсутствие поля не повышаются: объявление `Proven` не
делает доказанным номер, о котором адаптер промолчал.

### Старт по коду

Канал, чья ссылка не может нести транзакцию или чья платформа доставляет её не всегда, объявляет
[старт по коду](/docs/ru/guides/custom-channel-adapter#старт-по-коду-—-когда-на-ссылку-нельзя-положиться) в
`Http:Capabilities:CodeStart` — теми же двумя членами, что встроенный канал задаёт в коде. Ядро
показывает код на своих страницах, пользователь отправляет его вашему боту, а адаптер отправляет его
событием `auth_start_by_code` (см. [Wire-контракт](#wire-контракт)).

| Ключ (под `Http:Capabilities:CodeStart`) | Значения | Не задан |
| - | - | - |
| `LinkCarriesTransaction` | `false` — ваш `deep-link` возвращает ссылку, которая только открывает чат, и код — единственный способ начать вход; `true` — ссылка несёт транзакцию, а код — запасной старт рядом с ней. **Заданный ключ и объявляет старт по коду** | старта по коду нет |
| `MessageTemplate` | сообщение, которое отправляет пользователь; `{code}` стоит на месте кода ровно один раз | пользователь отправляет сам код; пустое значение считается незаданным |

```json appsettings.json theme={null}
"Capabilities": {
  "CodeStart": {
    "LinkCarriesTransaction": false,
    "MessageTemplate": "/veriqa {code}"
  }
}
```

```bash theme={null}
Veriqa__Channels__acme-chat__Http__Capabilities__CodeStart__LinkCarriesTransaction=false
Veriqa__Channels__acme-chat__Http__Capabilities__CodeStart__MessageTemplate="/veriqa {code}"
Veriqa__Channels__acme-chat__Http__UnmatchedCodeReplyText=<Natural Key вашего ответа>
```

`Http:UnmatchedCodeReplyText` — ответ на код, не называющий живого входа; не задан — отвечает
`Http:StaleLinkReplyText`. Шаблон проверяется там, где канал показывается, как у встроенного канала, а
не на старте. Старт останавливают три ошибки конфигурации: `CodeStart` одним значением вместо секции,
`LinkCarriesTransaction` не `true` и не `false`, `MessageTemplate` без `LinkCarriesTransaction`. При
`LinkCarriesTransaction: true` блок запасного кода и его настройка `SpareStartCodeEnabled` работают у
HTTP-канала, как у любого канала. `auth_start_by_code` от регистрации, не объявившей старт, не
обрабатывается.

<Note>
  Пока старт по коду доступен только регистрации без владельца. Регистрация, которая его объявила и
  задала `Http:Tenant`, останавливает хост на старте с сообщением, называющим оба ключа.
</Note>

## Wire-контракт

Каждый вызов Veriqa — подписанный `POST` JSON-конверта на `Http:BaseUrl`: `version`, `operation`,
`channel_type` (один вход может обслуживать несколько регистраций), `tenant` (или `null`),
`adapter_settings` (или `null`) и `payload`. Ответ — `200` с конвертом из `version` и ровно одного из
двух: `result` или `error` (`code` и `message`).

| Операция | Что делает адаптер |
| - | - |
| `send-prompt` | Отправляет вопрос подтверждения с двумя кнопками. Veriqa передаёт отрендеренный ею текст, подписи кнопок и структурный контекст — рендерить можно и самому |
| `send-message` | Отправляет пользователю сообщение. Его `kind` — один из объявленных регистрацией. Вид `phone_number_request` приходит с членом `phone_number_request`: подписи кнопки «поделиться» и второй кнопки, второе действие (`skip` или `cancel_sign_in`) и транзакция; как показать обе — решает адаптер. У остальных видов член равен `null` |
| `report-outcome` | Показывает пользователю, где находится транзакция: `outcome` — её итог либо `awaiting_claims` — вход подтверждён и ждёт данных, которые ещё запрашивает приложение. Итог может нести `phone_request_message` — запрос номера телефона, который пора снять. `expired` сообщается по каждой транзакции, начатой из канала, — с `prompt_message`, равным `null`, если вопрос в чате не задавался или `send-prompt` не вернул ссылку. `declined` приходит и без нажатия — когда пользователь отменяет вход на окне входа, пока вопрос ждёт в чате, — с `prompt_message`, равным `null`, если `send-prompt` не вернул ссылку. Вызывается, только если канал объявил `DeliversOutcomeNotice`; на нажатие по входу, который уже ждёт, `awaiting_claims` уходит, только если канал объявил ещё и `SupportsMessageUpdate` |
| `deep-link` | Возвращает `url`, который ведёт пользователя в ваш канал по транзакции |
| `health` | Сообщает, здоров ли адаптер, его версию и наибольшую поддерживаемую версию провода |

| Событие | Когда адаптер его отправляет |
| - | - |
| `auth_start` | Пользователь пришёл в канал по deep link транзакции |
| `auth_start_by_code` | Пользователь отправил боту код со страницы входа — только у регистрации, объявившей [старт по коду](#старт-по-коду). `code` — то, что прислал пользователь, без обёртки вашего `MessageTemplate` и без прочих изменений: пробелы и дефисы отбрасывает и длину проверяет Veriqa. Транзакции событие не называет |
| `auth_confirm` | Пользователь нажал кнопку подтверждения |
| `auth_decline` | Пользователь нажал кнопку отказа |
| `phone_shared` | Пользователь поделился номером телефона. `phone_number` — в E.164 с ведущим `+`: `+` и от 1 до 15 цифр, первая не ноль, без разделителей и пробелов. Номер в другой записи Veriqa не исправляет: событие не обрабатывается, ответ остаётся `200`, в лог пишется предупреждение |
| `phone_step_action` | Пользователь выбрал второе действие запроса номера — элементом управления, который несёт payload. `action` — `skip` или `cancel_sign_in`, как назвал запрос; `transaction_id` — транзакция запроса |
| `unaddressed` | Личное сообщение от известного отправителя без транзакции. Необязательный `text` несёт текст сообщения: кнопка без payload приходит текстом своей подписи — так платформа с такими кнопками отвечает на второе действие запроса номера |
| `unrelated` | Всё, на что Veriqa отвечать незачем |

Поля каждого `payload`, результата и события — в машиночитаемой
[JSON-схеме провода](#conformance-набор); это руководство их не повторяет. Правила, которых схема не выражает:

* **Коды ошибок.** Коды, которые называет провод, — в схеме, и любой другой код тоже принимается. Код
  `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](https://www.standardwebhooks.com/) `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` — в прогон не входят: ссылка, которая только открывает чат,
ничего не начинает. Подтверждение после старта кодом набор тогда не проигрывает — за придуманным кодом
транзакции нет, — поэтому этот путь проверяйте живым прогоном.

Набор — контейнерный образ, собирается из корня репозитория:

```bash theme={null}
docker build -f src/core/Veriqa.Core.ChannelAdapter.Http.Conformance/Dockerfile -t veriqa-http-channel-conformance .
```

Запускайте его рядом с адаптером, с конфигурацией канала теми же ключами `Veriqa__Channels__<type>__*`,
что читает Veriqa, и собственными ключами под `Conformance__*`: обязательны `ChannelType` и
`ChannelUserId`; `Mode`, `Tenants` и `ReportPath` — по желанию.

| `Conformance__Mode` | Что выполняется | Для чего |
| - | - | - |
| `calls` (по умолчанию) | только вызовы | CI-гейт вашего адаптера |
| `interactive` | вызовы и приёмник; deep link на платформе вы открываете и кнопки жмёте сами | ручная проверка половины событий |
| `automatic` | вызовы, приёмник и заглушка платформы, играющая пользователя; обязателен `Conformance__PlatformStubUrl` | адаптеры, у которых такая заглушка есть; её контракт — в README набора |

В `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 из своей папки:

```bash theme={null}
cp .env.example .env    # заполните секрет транспорта и креды платформы
docker compose up
```

Вместе они показывают все операции и все события своего канала, подпись в обе стороны,
идемпотентность (повтор не шлёт второго сообщения), учётные данные по тенанту, `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` внешнего адаптера

Правило раздела [Объявите уровень проверки своего канала — и задокументируйте его](/docs/ru/guides/custom-channel-adapter#объявите-уровень-проверки-своего-канала-—-и-задокументируйте-его)
действует для внешнего адаптера без изменений: трафик платформы проверяет ваш адаптер, поэтому
**значение для каждого режима приёма своего канала называете вы** в своей документации, а оператор
объявляет его в секции канала. Умолчания нет. Подпись транспорта между адаптером и Veriqa проверяется
всегда, и это значение её не описывает.

Референсные адаптеры называют свои:

| Адаптер | Режим приёма | `InboundVerification` |
| - | - | - |
| Telegram | `webhook` — секретный токен в заголовке каждого события | `shared_secret` |
| Telegram | `polling` — адаптер сам забирает события | `outbound_fetch` |
| WhatsApp через Twilio | вебхук, подписанный Twilio | `signature` |
| WhatsApp через Baileys | собственная исходящая сессия WhatsApp Web адаптера | `outbound_fetch` |

В той же документации перечислите ключи верхнего уровня своего `raw_metadata`, несущие персональные
данные, включая нативный ID пользователя платформы: по этому перечню интегратор решает, что ему можно
хранить.


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