Skip to main content
Veriqa читает стандартный IConfiguration хоста, поэтому любая настройка может прийти из appsettings.json, переменных окружения, user-secrets или любого другого провайдера. Всё, что принадлежит Veriqa, живёт под единственным корневым ключом Veriqa: Справочник отвечает на вопрос «чем это настраивается». На соседние два вопроса отвечают отдельные страницы: что вообще поддерживается из спецификаций — Поддержка OIDC и OAuth 2.0, чего сегодня нет — Известные ограничения. В переменных окружения : превращается в __ — Veriqa:OpenIddict:Database:Provider это Veriqa__OpenIddict__Database__Provider. Здесь стоит знать про одно ограничение оболочки. В имени переменной POSIX-оболочка допускает только буквы, цифры и _, поэтому export отвергает имя целиком, как только - или . содержит любой сегмент пути: export Veriqa__ChannelDisplay__Hints__my-channel=... отклоняется как недопустимый идентификатор. Откуда взялся такой сегмент, роли не играет. Это может быть значение, которое выбираете вы: тип канала в Veriqa:AuthPageDesign:QrCode:PixelsPerModuleByChannel:<тип канала> и в Veriqa:ChannelDisplay:Hints:<тип канала> (типу канала SPI-адаптера дефис разрешён) или код записи в каталоге Veriqa:UiConfigurations:Records. Ровно так же это может быть фиксированное имя ключа схемы, которое вы не выбираете вовсе, — вид сообщения в Veriqa:MessageTemplates:confirmation-prompt. Само имя переменной при этом законно, и ограничение обходится штатными средствами: Docker (-e), переменные Kubernetes (env:), launchSettings.json и вызов env ИМЯ=ЗНАЧЕНИЕ команда такие имена принимают, а .NET читает их как обычно. Путь, все сегменты которого состоят из букв, цифр и _, задаётся переменной окружения обычным порядком. Отдельный каталог конфигурации. Одна переменная задаёт не настройку, а место, откуда настройки читать: VERIQA_CONFIG_DIRECTORY указывает на каталог, и найденные там appsettings.json и appsettings.<Environment>.json хост накладывает поверх файлов, поставляемых с программой, и под переменными окружения. Оба файла необязательны; несуществующий каталог останавливает старт, пустой ошибкой не является. Так хранит свою конфигурацию установка службой ОС — /etc/veriqa на Linux, %ProgramData%\Veriqa на Windows, — там её не заденет замена программы.
Конфигурация валидируется на старте. Неизвестное значение enum, реляционный провайдер без строки подключения или включённый канал без токена останавливают приложение немедленно, а не падают позже на первом входе пользователя.

Veriqa:OpenIddict

OIDC-сервер, его хранилище и зарегистрированные клиенты.

Database

Умолчания у Provider нет: незаданное значение — не «InMemory по умолчанию», а несделанный выбор. Вне окружения Development такой хост не стартует (проверка на старте называет лечение), в Development — поднимается с Warning. InMemory остаётся допустимым значением, но задаётся явно, и каждый рестарт инвалидирует выданные токены — подходит для разработки и для коннектора на одном инстансе. Во встроенном режиме эти ключи провайдер не выбирают — там его выбирает ваш хост вызовом authServer.UseOpenIddictDatabase(...) либо authServer.UseInMemoryOpenIddictStore(); в self-hosted-режиме выбор задаётся этими ключами.

Server

Поведение зависит от окружения, и это важнее, чем кажется:
  • Development — без явных сертификатов генерируются dev-ключи. Удобно локально, но каждый рестарт даёт новые ключи, и несколько инстансов их не разделяют.
  • Любое другое окружение — dev-ключей нет: сервер не стартует, пока не заданы оба сертификата (подписи и шифрования). Отката на автогенерацию не существует.
Готовый Docker-образ запускается с ASPNETCORE_ENVIRONMENT=Production, то есть под второе правило. Если вы видите сгенерированные dev-ключи в развёрнутой установке — значит процесс поднят в Development, и это стоит исправить до выкатки.

Clients

Veriqa:OpenIddict:Clients — массив приложений, которым разрешено выполнять вход: Рядом с массивом клиентов — гейт развёртывания:

Veriqa:TransactionEngine

Жизненный цикл транзакции входа.

Store

Про миграции и ретенцию — см. Хранилища.

Veriqa:ScopesClaims

Какие scope’ы существуют, какие claim’ы они несут и какие claim’ы требуются для входа.
Непустой Scopes замещает собственный набор scope’ов продукта целиком: scopes_supported discovery-документа перечисляет ровно scope’ы каталога (плюс openid, который регистрируется всегда), а запрос любого другого scope отклоняется с invalid_scope. Если ваши клиенты используют offline_access, channel или avatar, перечислите их в каталоге явно.
Ключи claim в Claims:

Veriqa:AuthPageDesign

Внешний вид страницы входа — полный разбор в Конфигурации и кастомизации.

Veriqa:AuthPageDesign:QrCode

QR-код на странице входа: показывать ли его и в каком разрешении рисовать. Секция задаётся на трёх уровнях владения, и побеждает ближайший заданный: запись ui_config → клиент (Veriqa:OpenIddict:Clients[].QrCode) → эта глобальная секция. Показ QR и разрешение — две разные настройки, и уровень выигрывает каждую отдельно: клиент, задавший только ShowQrCode, оставляет разрешение глобальным. Разрешение при этом — одна настройка, заданная по каналам: PixelsPerModuleByChannel и PixelsPerModule — две записи одного и того же уровня, и уровень исчерпывает их обе, прежде чем разрешение начнёт искаться уровнем ниже. Практическое следствие: клиент, задавший только PixelsPerModule, выигрывает разрешение для всех каналов — карта по каналам из глобальной секции до него не доходит. Чтобы у канала осталось своё разрешение, назовите его в карте того же уровня, который задаёт PixelsPerModule. Пример. Глобальная секция задаёт PixelsPerModule: 6 и PixelsPerModuleByChannel с записью telegram: 14, а клиент app-client — только PixelsPerModule: 8: PixelsPerModule вне диапазона на уровне клиента или записи ui_config не применяется — разрешение приходит с уровня ниже, старт не падает. Про такое значение сообщает одно предупреждение в логе на снимок конфигурации: весь каталог значений (глобальная секция, записи клиентов, записи ui_config) проверяется при старте и при каждой перезагрузке конфигурации, а не на каждый запрос. В глобальной секции такое значение остаётся ошибкой развёртывания и останавливает старт.

Veriqa:Localization и Veriqa:ChannelDisplay

Veriqa:RateLimit

Per-endpoint лимиты, каждый — число разрешений за окно в секундах. Дефолты: Лимиты вебхуков заслуживают внимания при масштабировании: они применяются на инстанс, и агрессивно ретраящий провайдер канала может их выбить.

Veriqa:InitiatorContext

Контекст об устройстве, начавшем транзакцию, показываемый пользователю до подтверждения — см. Контекст инициатора.
Offline-путь GeoIP ожидает базу GeoLite2, которую Veriqa не поставляет. Скачайте её у MaxMind сами и примите их EULA — библиотека Apache-2.0, лицензия лежит на данных. Саму библиотеку тоже надо подключить: она едет сателлитным пакетом Veriqa.Core.AuthServer.MaxMind (builder.Services.AddMaxMindGeoIp()), в базовую поставку не входит.

Veriqa:HopMode

Hop-режим кладёт в QR-код короткую одноразовую ссылку на ваш сервер Veriqa вместо ссылки канала. Две его настройки задаются на весь узел и могут быть переопределены для отдельного канала: Заданное значение канала побеждает значение узла; отсутствующее или пустое — наследует его. Остальные ключи HopMode.* и их дефолты — в разделе Ключи в каталоге объявлений, где у HopMode.Enabled и HopMode.RedirectStyle стоит «not declared»: их дефолты — значения выше.

Инфраструктура

Плоские ключи, читаемые хостом, в основном актуальны при запуске более одного инстанса: Если не задан ни один вариант DataProtection, ключи живут в памяти и теряются при рестарте — нормально для разработки, сломано для масштабируемого деплоя.
Лицензирование Redis. Redis server ≥ 7.4 поставляется под RSALv2/SSPL (8.x дополнительно предлагает AGPL). На лицензию Veriqa это не влияет, но если влияет на вашу политику — Valkey (BSD) является drop-in альтернативой.

Veriqa:Channels

Каждый канал живёт под Veriqa:Channels:{Channel} и активируется только с "Enabled": true. Полная настройка с шагами на стороне провайдера — в Настройке каналов.

InboundVerification — обязательный ключ каждой канальной секции

Veriqa:Channels:{Channel}:InboundVerification объявляет, как проверяется подлинность входящего события этого канала. Значение выбирается из закрытого словаря:
Ключ обязателен для каждого включённого канала. Развёртывание, где включён канал, объявленный на уровне ядра ("Enabled": true), а значение не задано или не входит в словарь, не стартует: сообщение назовёт тип канала, адрес ключа и все четыре допустимых токена. Значение стоит проставить и в секциях выключенных каналов — тогда переключение Enabled в true не уронит старт. Стартовая проверка перебирает ровно тот набор каналов, из которого ядро строит список включённых: все четыре поставляемых канала в нём есть, а канал, подключённый через SPI одним вызовом AddChannel, в него не попадает — ключ в его секции тоже нужен, но без него старт не остановится, см. Свой канальный адаптер.
Факт объявляете вы, а не адаптер и не ядро: Veriqa видит только то, что проверка выполнилась и прошла, но не то, чем именно она была, — а вывод значения по типу канала означал бы, что сторонний канал описать нечем. Значение для каждого встроенного канала и режима — в Настройке каналов. Канал, забирающий обновления сам (UpdateMode: Polling), но объявивший что-то кроме outbound_fetch, даёт Warning при старте — старт при этом продолжается. Объявленное значение доезжает до записи журнала аудита отдельным атрибутом, поэтому по журналу видно, чем было защищено подтверждение.

OutcomeNotice — где появляется квитанция исхода

Veriqa:Channels:OutcomeNotice:DisplayIntent (ключ Channels.OutcomeNotice.DisplayIntent) говорит, как вы хотите видеть квитанцию терминального исхода в переписке. Значение задаётся на трёх уровнях — ядра, тенанта и приложения, — и каждый свободно перекрывает нижний: глобальная секция задаёт намерение всей установки, тенант — своё, членом OutcomeNoticeDisplayIntent своей записи, отдельное приложение — членом с тем же именем в своей записи OIDC-клиента. Запись ui_config намерения не задаёт. Какой бы уровень ни ответил, значение действует на исход целиком — подтверждение, отказ и истечение показываются одинаково. Поставочное значение — ReplacePrompt: ровно то, что каналы делали до появления настройки, поэтому развёртывание, которое ничего не задало, изменения не увидит. Нечитаемое значение читается как незаданное и отвечает тем же ReplacePrompt; старт при этом не прерывается.
Это намерение, а не гарантия. Telegram и MAX его исполняют. WhatsApp всегда отправляет новое сообщение, а Email квитанцию исхода не показывает вовсе — так они устроены в сегодняшней поставке. Адаптер, не исполнивший намерение, не ошибается: ошибка не пишется в журнал, результат доставки не меняется. При NewMessage квитанция приходит отдельным сообщением, а кнопки вопроса снимаются тем же ходом — там, где платформа умеет сменить кнопки сообщения, не переписывая его текст; формулировка вопроса при этом не переписывается никогда. Неудача снятия доставку тоже не ломает — и если кнопка уцелела, повторное нажатие принесёт ещё одну квитанцию, потому что терминальную транзакцию переиграть нельзя.

Veriqa:MessageTemplates

Всё, что пользователь читает в мессенджере и в письме, — сообщения. Одно сообщение это две настройки, а не одна текстовка: контракт объявляет, какие слоты у сообщения есть, а шаблон несёт формулировки, которые этими слотами пользуются. Поэтому и адресов два — на сообщение, а не на каждый текст: Группы в квадратных скобках необязательны: формулировка, которая не сужается по оси, опускает её группу целиком — так один адрес обслуживает все шаги подстановки, от самого специфичного ({вид, тип транзакции, тип действия, поверхность, канал}) до самого общего ({вид}). Вид сообщения из группы не выпадает никогда: «текст какого сообщения» — это и есть сам вопрос. Одна группа не самостоятельна: ByAction читается только внутри ByType. Ни одна ступень подстановки не называет тип действия без типа транзакции, поэтому формулировка по адресу {вид}:ByAction:{тип действия} недостижима — хост на ней не стартует с сообщением, что адрес «narrowed by a combination of axes no step of its ladder addresses». Тип действия подтверждения поэтому формулируется по адресу {вид}:ByType:confirmation:ByAction:{тип действия}. Виды, которые везёт поставка: Значение шаблона — лестница ступеней от самой полной к минимальной, одним массивом. Ступень это либо строка (текст, годный любому приёмнику), либо структура { "Subject": "…", "Html": "…", "Plain": "…" }, где Html и Plain — редакции ступени: письмо формируется одно и несёт обе части сразу. Каждая редакция необязательна, но ступень обязана заявить хотя бы одну — иначе отказ на старте с указанием рода сообщения и номера ступени. Показывается первая ступень, заявившая запрошенную редакцию и все слоты которой заполнены; поэтому две части одного письма могут прийти с разных ступеней, а тема берётся у ступени, выбранной для Html. Редакцию, которую не заявила ни одна ступень, письмо не несёт вовсе — так объявляется «письмо только в html». Минимальная ступень обязана опираться только на гарантированные слоты — это проверяется на старте, на каждом шаге и по каждой редакции отдельно. Шаг, объявивший лестницу, заменяет её целиком: ступени разных шагов не смешиваются. Формулировка из поставки может стоять по более узкому адресу, чем та, что пишете вы. Подстановка идёт уровень за уровнем, и уровень исчерпывает все ступени адреса — от самой специфичной до самой общей — прежде чем резолюция спустится на уровень ниже. Поставляемые объявления — это фолбэк уровня Core, читаемый по тем же адресам; а Veriqa:MessageTemplates в вашем appsettings.json — это и есть тот самый уровень. Поэтому ваше объявление по адресу шире поставочного не срабатывает вовсе: первой отвечает более узкая ступень, а отвечает она из поставки. Ничего не падает и ничего не пишется в лог — конфигурация корректна, её просто никто не спрашивает. (Запись выше ядра — другой уровень и читается раньше, так что формулировка в записи клиента так не перекрывается, но и действует только на этого клиента.) Первыми с этим встречаются квитанции исхода: каждый из трёх видов везёт формулировку по голому {вид} и более узкие — для входа: outcome-receipt-declined поставляется по тем же четырём адресам; outcome-receipt-expired — по этим четырём плюс ступень на ByType:login. Квитанция входа, показанная в мессенджере, спрашивает про {вид, login, in-channel-messenger} — и про канал, по которому ни одна поставочная квитанция не сужается, — поэтому переобъявление outcome-receipt-confirmed:Templates там не меняет ничего. Формулируйте по тому же адресу, который занят поставкой:
appsettings.json
Контракт остаётся в корне вида — он сужается по типу транзакции и типу действия и никогда по поверхности, — а лестница заканчивается ступенью без слотов: ни один слот квитанции не гарантирован (см. ниже). Квитанция подтверждения — уже другой адрес: под ByType:confirmation поставка не объявляет ничего, поэтому там отвечает как раз широкое объявление по {вид}, и переформулировка работает как написана. Поверхности, которыми адресуется квитанция: in-channel-messenger (канал, который рисует эмодзи, — мессенджеры, на которые продукт везёт адаптер, и любой канал, заявивший это о себе), in-channel-plain (канал, который не рисует), email-confirm-page (страница подтверждения Email-канала), core-page (страница подтверждения, которую Veriqa отдаёт сама) и interaction-page (страница, на которой остаётся пользователь при подтверждении без браузерного колбэка). Квитанции исхода — только простой текст. Три вида outcome-receipt-* поставляются одной редакцией — Plain, и общее правило «редакцию, которую не заявила ни одна ступень, письмо не несёт» к ним не относится: у квитанции часть одна, и не показать её значит не ответить пользователю. Поэтому лестница квитанции, переопределённая только редакцией Html, не даёт текста вовсе — это ошибка установки, а не другой вид текста: рендер отказывает и называет вид сообщения, подмены другой редакцией не происходит. Экранирование квитанции при вставке в страницу делает сама страница, так что простой текст у формулировки ничего не отнимает. Квитанция может назвать, что подтверждали. Поставляемый контракт трёх видов квитанций слотов не объявляет, поэтому поставляемые квитанции о действии ничего не говорят. Объявите caller-слоты в контракте квитанции на своём уровне — под ByType:confirmation:ByAction:{action type}, чтобы формулировать её по действию, — и квитанция заполнится из slot_values, с которыми создавалось подтверждение, по тем же именам. Caller-слот квитанции указывает MaxLength, как и в вопросе:
Значение, которого у транзакции нет (вход, подтверждение, созданное без него), исключает ступень, которая его использует, и отвечает следующая — поэтому минимальная ступень квитанции не считает caller-слот гарантированным, даже помеченный Required. Квитанция на решение из канала, которому вопрос не показывали, caller-значений не получает, как и квитанция на странице входа в подтверждение, если InteractionPage не входит в ConfirmationSubjectDisplay.Surfaces. Server-слоты, описанные ниже, заполняются и там, кроме outcome_at на странице входа. Квитанция может нести и то, что продукт знает о транзакции. Её контракт может объявить server-слоты app, browser, os, region (тип string, Source: ServerInitiatorContext) и outcome_at (тип datetime, Source: ServerSystem); caller-значение их не заполняет. Объявление одного из этих пяти имён с Source: Caller при старте не отклоняется, но слота вам не отдаёт: app всегда принадлежит продукту, а остальные четыре — ваши только на тех ветках, где у продукта своего значения нет. Возьмите собственное имя. app называет приложение: client_id у подтверждения, отображаемое имя клиента (или его client_id) у входа. browser, os и region берутся из контекста инициатора, поэтому есть только у входа, и подчиняются тем же Enabled и DisplayFields раздела Veriqa:InitiatorContext, что и промпт входа. outcome_at — момент решения, а у истёкшей транзакции — её срок; страница входа формулирует квитанции до исхода, поэтому там значения у него нет. Ни один из этих слотов в квитанции не гарантирован, даже помеченный Guaranteed, поэтому минимальная ступень на них опираться не может. Уровни у настроек разные. Контракт резолвится с Tenant, Application и Core; шаблон — оттуда же плюс запись ui_config. Так запись, которую выбирает параметр запроса, может выбрать, какую из ваших формулировок увидит пользователь, но не может ни расширить список слотов, ни завести собственный тип действия.
Уровень Tenant. Ридер уровня Tenant в поставке не регистрируется — его пишет интегратор. Без него резолюция идёт на Application/Core.
Уровень Application — это запись клиента. Выше ядра адрес отсчитывается от записи уровня, а запись Application — это запись клиента в Veriqa:OpenIddict:Clients: контракт сообщения для одного приложения лежит по адресу Veriqa:OpenIddict:Clients:[n]:MessageTemplates:{kind}:…:Contract. Action type подтверждений, которые создаёт ваш сервер, — самостоятельные виды сообщений, и объявляются они только выше ядра — в записи клиента, как MessageTemplates:{action type} с Contract и Templates. Вид, видимый только в секции хоста Veriqa:MessageTemplates, POST /api/transaction/confirmation отклоняет с action_type_unknown. Полный пример с запросом и ответом: API серверного подтверждения. Ось channel — уточнение поверхности, поэтому она самая специфичная и без surface не встречается. Поставка объявляет по ней два значения — обе формулировки предзаполненного сообщения под deeplink-prefill:BySurface:in-channel:ByChannel:whatsapp и …:ByChannel:email; у остальных видов значений по этой оси нет, и шаг без канала отвечает за все каналы сразу. Спрашивать ключ об оси, которой он не объявляет, — ошибка.

Health-эндпоинты

Readiness регистрирует по проверке на каждую настроенную зависимость, поэтому отражает ровно то, что вы подключили: нет настроенной БД — нет проверки БД.

Ключи в каталоге объявлений

Таблица ниже строится из каталога объявлений ключей: имя, тип значения, уровни, измерения и адрес на каждом уровне. Остальные ключи описаны в секциях выше. Ниже — ключи, которыми владеет Veriqa. Тот же каталог открыт и тому, что поставляете вы: своё расширение объявляет ключи на уровнях ядра, приложения и ui_config через тот же публичный API, и его объявления попадают в эту схему на старте — см. Свои ключи конфигурации. Колонка «Дефолт» печатает то, что объявил владелец ключа: чем отвечает уровень ядра, когда его запись ничего не заявляет. Ключ, такого ответа не объявивший, назван так прямо — его дефолты ищите в секциях выше. «Объявлен, значение не печатается» стоит там, где ответ объявлен, а текста у него нет: объявленный ответ равен null, либо ключ объявлен секретным — схема секретного ключа несёт факт ответа и никогда его текст. Значения, которое поставила установка, в таблице нет ни в одной колонке: здесь напечатано объявление, а не резолюция.

Дальше

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

MAX, Telegram, WhatsApp, другие мессенджеры и Email — от начала до конца.

Продакшн-харденинг

Что проверить перед выкаткой.