/v1/admin — поверхность, которую установка автоматизирует общим bearer-токеном
из скрипта, задания CI или другого сервиса.
Объединение любых двух из них означало бы, что один молча наследует middleware
другого, а учётные данные клиента никогда не должны дотягиваться до
операторского маршрута. Структурное разделение делает это свойством таблицы
маршрутов, а не проверкой, о которой кто-то должен помнить, и есть тест,
утверждающий, что клиентский маршрутизатор отвечает
404 на
/v1/panel/auth/session, /v1/admin/orders и /v1/panel/admins.
Когда использовать /v1/admin, а не /v1/panel
Используйте /v1/admin, когда вызывающая сторона — не браузер. Здесь нет сессии,
которую нужно установить, нет CSRF-токена, который нужно держать в согласии с
ротацией, и нет хранилища cookie. Это единственный скриптуемый способ создать
заказ, принять платёж, зарегистрировать возврат, скорректировать кошелёк,
запустить импорт из Remnawave и поставить в очередь операцию выдачи доступа.
Используйте /v1/panel, когда вызывающая сторона — человек, или когда вам нужно
то, что /v1/admin не предоставляет: служба поддержки, маркетинговые кампании,
редактор каталога, настройки установки, журнал аудита, разбор рисков. Это также
единственная поверхность с авторизацией по отдельным возможностям: интеграция,
которая должна уметь читать, но не оформлять возвраты, живёт за учётной записью
оператора с узкой ролью, а не за общим токеном.
Каждый изменяющий состояние запрос на этом префиксе — всё, кроме GET, HEAD и
OPTIONS, — после обработки записывается в журнал аудита с типом актора
operator и идентификатором operator-token, действием admin_api.<метод>
(например, admin_api.post), категорией system, исходом (failure для любого
4xx или 5xx), а также шаблоном маршрута и статусом в метаданных. Токен не
называет человека, поэтому журнал не может сказать кто; но он говорит, что
изменение сделал общий токен, а не оператор в панели, и по этому актору можно
фильтровать в браузере аудита. Чтения и запросы, отклонённые из-за неверного
токена, не записываются.
Ничто внутри этого репозитория не вызывает /v1/admin: Telegram-бот работает с
PostgreSQL и Remnawave внутри процесса. Этот префикс существует исключительно как
поверхность для внешних интеграций и автоматизации.
Аутентификация
ЗадайтеAPP_OPERATOR_TOKEN длинным случайным секретом и передавайте его в каждом
запросе:
401 unauthorized.
API требует APP_OPERATOR_TOKEN, чтобы вообще собрать свой коммерческий
рантайм, наряду с 32-байтным APP_DATA_ENCRYPTION_KEY, APP_REMNAWAVE_URL,
APP_REMNAWAVE_TOKEN и APP_VALKEY_URL. Он не является необязательным даже для
установки, которая никогда не обращается к этому префиксу, — см.
Конфигурацию.
Поскольку сессии нет, некоторые обработчики принимают действующего оператора как
данные, которые сервер не может проверить: operatorId в теле
PUT /v1/admin/maintenance (400 missing_operator, если он отсутствует) и
operatorID в решениях по ручным платежам и корректировках кошелька. Эти значения
попадают в запись. Передавайте что-то осмысленное и не считайте их
аутентификацией.
Правила запросов
Content-Type: application/jsonв любом запросе с телом.- Тела декодируются с отклонением неизвестных полей и должны содержать ровно
одно значение JSON. Лишнее поле «на будущее» или данные после значения дают
400 invalid_request. - Размер тела ограничен 1 МиБ.
- Обработчики
/v1/adminдекодируют данные в структуры Go без JSON-тегов, поэтому имена полей сопоставляются без учёта регистра:customerID,customerIdиCustomerIDработают одинаково. Схемы OpenAPI называют их в camelCase; принимаются оба варианта. - Ко всем маршрутам применяется серверный таймаут обработчика в 60 секунд.
Ключи идемпотентности
Каждое изменение, порождающее движение денег, выдачу доступа или проводку в книге, принимает заголовок запросаIdempotency-Key. Это операции, в которых дважды
отправленная форма, повтор запроса прокси или переотправленный вебхук не должны
породить два заказа, два возврата, два зачисления в кошелёк или двух пользователей
Remnawave.
Генерируйте одну уникальную строку на одно логическое действие и повторно
используйте её дословно при каждой повторной попытке этого действия. Ключ
пробрасывается в доменный слой и именно он делает нижележащий SQL идемпотентным;
общего кэша повторов на уровне middleware нет, поэтому ключ, переиспользованный для
двух разных действий, — ошибка вызывающей стороны.
Валидация — это исключительно проверка длины: от 8 до 128 символов, без правил на
набор символов. Отсутствующий или выходящий за диапазон ключ на /v1/admin —
400 missing_idempotency_key.
Контракт помечает
Idempotency-Key как обязательный для
POST /v1/admin/payments/{paymentID}/reconcile, но обработчик его никогда не
читает. Сверка идемпотентна сама по себе — она заново опрашивает провайдера
через платёжный сервис. Отправка заголовка безвредна./v1/account отвечает 400 idempotency_key_required (кроме
POST /v1/account/shop/purchase, который отвечает 422), а /v1/panel отвечает
422 idempotency_key_required и принимает любую непустую строку без проверки
длины. Вебхуки провайдеров дедуплицируются по собственному идентификатору события
провайдера, а не по этому заголовку.
Курсорная постраничная выборка
Списки, которые могут расти неограниченно, возвращают{"items":[…], "nextCursor":"…"} и принимают ?cursor=<value>&pageSize=<n>.
Пустой nextCursor означает последнюю страницу. Это постраничная выборка по ключу
(keyset) по (timestamp, id), а не через OFFSET: кодирование ключа сортировки
удерживает границу страницы стабильной, пока вставляются новые строки, чего смещение
обещать не может — иначе интеграция, обходящая журнал аудита во время поступления
новых событий, видела бы строки дважды или пропускала их.
Запросите первую страницу без cursor, передавайте возвращённый nextCursor
обратно дословно и остановитесь, когда он пуст. Считайте значение
непрозрачным. В кодовой базе оно кодируется тремя разными способами — base64url,
открытый текст и пара параметров cursor + cursorId — и возврат значения без
изменений корректен во всех случаях, а его разбор — нет.
Некоторые списки /v1/admin вообще не используют курсоры и принимают обычный
limit:
Ответы об ошибках
Каждый отказ на всех трёх префиксах формируется одной функцией и возвращается в формате RFC 9457:Content-Type — application/problem+json. Ветвитесь по последнему сегменту
пути в type — это стабильный машиночитаемый код. По одному только статусу
нельзя отличить «промокод истёк» от «промокод опускает цену ниже себестоимости», а
detail — это проза, которая меняется, когда кто-то улучшает формулировку.
title — всегда текст HTTP-статуса, никогда не собственный заголовок.
request_id записан в snake_case, тогда как все остальные поля API — в camelCase.
Так пишет код и так объявляет схема.
Коды, которые стоит обрабатывать явно:
Одно и то же нижележащее условие может приходить с разными кодами на разных
префиксах: недоступность Remnawave при получении списка устройств — это
502 remnawave_unavailable на /v1/panel и 503 upstream_unavailable на
/v1/account. Инструкция, которая трактует «Remnawave лежит» как одно условие,
должна знать оба кода.
Два вида отказов полностью выпадают из формата problem: коллектор телеметрии
отвечает обычным текстом, а GET /v1/panel/audit/export отдаёт CSV потоком уже
после того, как записан 200, поэтому сбой в середине потока обрезает файл, а не
порождает тело с описанием проблемы.
Идентификаторы запросов
Каждому запросу присваивается идентификатор, и каждый ответ несёт его вX-Request-ID. То же значение появляется как request_id в телах ошибок, как
requestId в строках аудита, в идентификаторах корреляции выдачи доступа и в
событиях безопасности клиента.
Считывайте его из каждого ответа и записывайте в лог. Это единственная строка,
которая связывает «клиент говорит, что платёж не прошёл» со строкой лога
обработчика, строкой аудита, операцией выдачи доступа и вызовом Remnawave, — и при
этом никому не нужно передавать идентификатор клиента по каналу поддержки.
На /v1/panel идентификатор запроса ещё и служит единицей дедупликации для дважды
отправленной операторской формы: операция над подпиской выводит свой ключ
идемпотентности как panel:<subscriptionID>:<operation>:<requestID>.
Ограничения частоты запросов
Ограничения реализованы как счётчики с фиксированным окном в Valkey. К/v1/admin
относится только одно из них:
Больше ничто на
/v1/admin не ограничивается по частоте. Заголовка
Retry-After нет, как нет и заголовков с состоянием лимита в ответах, поэтому
клиент, упёршийся в ограничение, должен выдерживать паузы по собственному
расписанию.
Ограничения срабатывают на отказ. Если Valkey недоступен, ограничитель
возвращает ошибку и запрос отклоняется, потому что сбой не должен молча снимать
защиту. На пути промокода это 503 rate_limit_unavailable; на путях входа
оператора это означает, что вход полностью заблокирован, пока Valkey лежит.
Адрес клиента, по которому считается ограничение, берётся из разрешения доверенных
прокси, описанного в Безопасности. При пустом
APP_TRUSTED_PROXIES заголовок X-Forwarded-For игнорируется и используется адрес
сокета — а значит, развёртывание за прокси, где эта переменная забыта, ограничивает
всех вызывающих как одного субъекта.
Заголовок X-Operator-Reason
Этот заголовок принадлежит /v1/panel, а не /v1/admin. Он читается в каждом
запросе /v1/panel и несёт причину, по которой оператор вносит изменение:
причина едет в заголовке, а не в каждом теле, потому что она нужна почти каждому
изменяющему запросу здесь, и протаскивание её через шестьдесят структур запросов
гарантированно привело бы к тому, что в одной из них о ней забудут. Значение
попадает в журнал аудита, а для некоторых записей — и в саму запись.
Отправляйте X-Operator-Reason: <1..400 символов> в тех изменяющих запросах
/v1/panel, которые её требуют: предпросмотр массовой операции, отзыв подарка,
изменение статуса и жизненного цикла клиента, регистрация возврата, завершение или
отмена выдачи товара, ручное переключение режима обслуживания, решения по
блок-листу и апелляции, разбор аномалий и добавление клиента в белый список рисков.
Отсутствующая причина возвращается как 422 invalid_request с деталью
«Those details are not valid, or a reason is required.»
Обработчики /v1/admin этот заголовок игнорируют. Там, где им нужно обоснование,
они принимают его в теле — reason в
POST /v1/admin/customers/{customerID}/lifecycle,
в POST /v1/admin/orders/{orderID}/cancel и в PUT /v1/admin/maintenance.
Разобранный пример
Создадим заказ для существующего клиента и запустим по нему платёж.checkoutUrl. Вебхук провайдера завершает платёж на
POST /v1/payments/webhooks/{provider}; больше от интеграции ничего не требуется.
Опрашивайте GET /v1/admin/orders/{orderID}, если вам нужно наблюдать за
результатом, а конечный автомат описан в
Заказах и платежах.
Повтор любого из этих вызовов с тем же Idempotency-Key возвращает запись,
созданную первой попыткой, а не создаёт вторую. Повтор с новым ключом создаёт
второй заказ.
Что покрывает /v1/admin
Тридцать эндпоинтов, сгруппированных по назначению.
Что они намеренно не возвращают:
GET /v1/admin/webhooksникогда не отдаёт исходное тело от провайдера. Это собственная полезная нагрузка провайдера, и в ней могут быть платёжные данные; для ответа на вопрос «пришло ли это и было ли принято» достаточно статуса и результата проверки подписи.GET /v1/admin/jobsвозвращает сводку аргументов задачи, а не сами аргументы, и прогоняет последнюю ошибку через маскирование чувствительных данных, поэтому ссылка на подписку или bearer-токен из сообщения вышестоящей системы никогда не покидает процесс.- Эндпоинт жизненного цикла применяет изменение на уровне изоляции
Serializableи соблюдает конечный автомат клиента:suspend— только изactive,restore— только изsuspended,anonymize— только изdeletedи только после того, как истёк срок хранения.
GET /v1/admin/jobsи маршруты retry/cancel отвечают503 jobs_unavailableв процессе, к которому не подключён клиент очереди задач. Фильтрstateпо умолчанию —dead_letter; остальные принимаемые значения —retryable,running,available,scheduledиcompleted.- Долговременное хранилище задач — River, чья схема поставляется вместе с
миграциями Atlas, поэтому на стандартной установке эти эндпоинты читают
собственные строки очереди River. Представление
/v1/panel/system/jobs, показывающее операции выдачи доступа, — другое: оно опирается на собственные таблицы Omniflow. GET /v1/admin/outboxсообщает количество неопубликованных доменных событий и возраст самого старого. Ничто в текущей сборке никогда не помечает строку outbox опубликованной, поэтому это число растёт и не является полезным сигналом о накопившемся отставании. См. Наблюдаемость.
Эндпоинты, не требующие учётных данных
Пять видов маршрутов отвечают без каких-либо учётных данных, и каждый защищён чем-то помимо аутентификации:/healthz,/livez,/readyz— пробы./readyzпубликует стабильную классификацию (timeout,unavailable), а не сообщение драйвера, поэтому не может раскрыть строку подключения./metrics— отдаётся только когдаAPP_METRICS_ENABLEDвключён, и предполагается, что доступ к нему ограничен на обратном прокси. Все метки ограничены по мощности: используется шаблон маршрута, а не конкретный путь с идентификаторами.GET /v1/catalog/plans?locale=ru|en— анонимное чтение витрины. Любая локаль, кромеen, откатывается кru.POST /v1/payments/webhooks/{provider}— не аутентифицирован по необходимости и защищён проверкой подписи провайдера. Неудачная проверка всё равно записывается и отвечает400 webhook_rejected; тело больше 1 МиБ —413 payload_too_large; повторное событие провайдера разрешается в{"status":"duplicate"}.POST /v1/telemetry/events— присутствует, только когдаAPP_TELEMETRY_COLLECTOR_ENABLEDвключён (по умолчанию выключен). Это приёмная сторона, которую разворачивает установка-коллектор, а не то, что открывает наружу обычное развёртывание.
Контракт OpenAPI
api/openapi.yaml — исходный контракт (OpenAPI 3.0.3), именно он рендерится в
публикуемом справочнике API. Из него генерируются и серверные типы Go, и
TypeScript-биндинги, а CI падает, если изменение контракта закоммичено без их
перегенерации.
Учитывайте эти пробелы, прежде чем генерировать по нему клиент:
- В нём описаны 113 из 195 маршрутов
/v1/panel. Целые области отсутствуют: настройки установки, реестры AI и MCP, служба поддержки, маркетинг, лояльность, OIDC для операторов, представления состояния здоровья и режима обслуживания. Поверхности/v1/adminи/v1/accountописаны полностью. - Ни одна операция не ссылается на схемы безопасности
panelSessionилиaccountSession. Схемы определены, а корневогоsecurityнет, поэтому в терминах OpenAPI каждая операция/v1/panelи/v1/accountчитается как неаутентифицированная. Только 30 операций/v1/adminнесутsecurity: [{operatorToken: []}]. Сгенерированный клиент не будет знать, что вызовам панели нужны учётные данные. - Требуемые права доступа указаны только в текстовых описаниях, поэтому никакой
инструментарий не может их извлечь. Авторитетный ответ — это
requirePermission(...)в точке монтирования каждого маршрута. - Ответы не валидируются по контракту во время выполнения. Обработчики декодируют данные в анонимные структуры и пишут вручную собранные map; сгенерированные серверные типы Go не импортируются нигде. Ответ может разойтись со своей схемой без падения тестов.
info.versionсодержит1.0.0. Он отражает линию выпусков, а не отдельную версию контракта, поэтому по нему нельзя понять, какие из перечисленных выше пробелов уже закрыты.- Две операции задокументированы безусловно, но в коде условны:
GET /metricsиPOST /v1/telemetry/events. В более общем случае маршрут/v1/panelили/v1/account, чей сервис не подключён, отсутствует (404), а не отдаёт503—503сообщил бы анонимному вызывающему, что именно настроил оператор.
Связанные страницы
Конфигурация
APP_OPERATOR_TOKEN, APP_TRUSTED_PROXIES и остальное окружение.Безопасность
Поверхности с учётными данными, что никогда не пишется в логи и как запечатываются секреты.
Заказы и платежи
Конечный автомат заказа, который приводит в движение разобранный пример.
Задачи и очереди
Что показывают эндпоинты задач и что на самом деле повторяется.