Перейти к содержанию

Admin API

Административные endpoint-ы Payment Manager. Используются для health-check Kubernetes-проб, runtime-управления логированием, отладки fee-правил и принудительного разрешения зависших intent-ов.

Аутентификация admin endpoint-ов

Все /admin/* маршруты защищены плагином adminPlugin.ts, который выполняет проверку bearer-токена в preHandler:

Authorization: Bearer <ADMIN_SECRET>
  • ADMIN_SECRET берётся из переменной окружения (см. src/shared/config.ts).
  • Сравнение токенов выполняется через crypto.timingSafeEqual — защита от timing-атак.
  • При отсутствии заголовка или несовпадении токена возвращается 401 UNAUTHORIZED.
  • Это отдельная от HMAC схема: обычные платёжные endpoint-ы (/intents, /intents/:id/confirm, …) используют HMAC (X-Service-Id, X-Timestamp, X-Signature); admin-эндпоинты — bearer-токен.

Исключение: GET /health не требует аутентификации — он вызывается Kubernetes-пробами и nginx-апстримом без заголовков.


1. GET /health

Файл: src/admin/health.ts:5.

Назначение

Единственный health-endpoint Payment Manager. Используется Kubernetes liveness/readiness-пробами и nginx-апстримом для проверки доступности сервиса.

Endpoint объединяет три проверки:

  1. Event loop pressure — через плагин @fastify/under-pressure, зарегистрированный в src/server.ts:84 с maxEventLoopDelay: 1000. Если задержка event-loop превышает 1000 мс — app.isUnderPressure() вернёт true.
  2. TigerBeetle reachability — выполняется getTb().lookupAccounts([]) (пустой lookup как ping). Если SDK недоступен или TB-соединение оборвано — 503.
  3. Базовая жизнеспособность процесса — если Fastify отвечает, процесс жив.

В проекте нет разделения на /healthz, /readyz, /startupz, /livez. Используется только /health.

Auth

Не требуется. Открытый endpoint.

Request

GET /health

Без параметров, без тела.

Response

200 OK — сервис здоров:

{
  "status":    "ok",
  "version":   "1.4.2",
  "timestamp": "2026-05-29T10:15:00.000Z"
}
  • version — из process.env.npm_package_version; "0.0.0" если не задана.
  • timestamp — ISO-8601 момента обработки запроса.

503 Service Unavailable — сервис деградирован:

{
  "status":  "ko",
  "message": "Service under pressure"
}

Возможные значения message:

  • Service under pressure — превышен maxEventLoopDelay (1000 мс).
  • TigerBeetle unavailablelookupAccounts бросил исключение.

Errors

Код Условие
503 Event loop > 1000 мс или TigerBeetle недоступен

curl

curl -sS http://localhost:3000/health

Ссылки


2. GET /admin/debug-level

Файл: src/admin/debug-level.ts:63.

Назначение

Возвращает текущий уровень логирования pino-логгера и (если активирован debug-режим) время автоматического возврата к info.

Используется как контроль перед/после POST /admin/debug-level.

Auth

Authorization: Bearer <ADMIN_SECRET>

Request

GET /admin/debug-level

Без параметров, без тела.

Response

200 OK:

{
  "level":    "debug",
  "revertAt": "2026-05-29T10:25:00.000Z"
}
  • level — один из "debug" | "info" | "warn" | "error".
  • revertAt — ISO-8601 момента, когда уровень автоматически вернётся к info. null, если активен дефолтный info-уровень (никаких таймеров не запущено).

Errors

Код Условие
401 Отсутствует/невалидный Authorization: Bearer

curl

curl -sS \
  -H "Authorization: Bearer $ADMIN_SECRET" \
  http://localhost:3000/admin/debug-level

Ссылки


3. POST /admin/debug-level

Файл: src/admin/debug-level.ts:30.

Назначение

Временно меняет уровень логирования pino runtime — без рестарта сервиса. По истечении durationMs уровень автоматически возвращается к info (через setTimeout).

Используется для диагностики продакшен-инцидентов: повышаем до debug на 10 минут, собираем трассы, ждём auto-revert.

Auth

Authorization: Bearer <ADMIN_SECRET>

Request

POST /admin/debug-level
Content-Type: application/json

{
  "level":      "debug",
  "durationMs": 600000
}
Поле Тип По умолчанию Описание
level "debug" \| "info" \| "warn" \| "error" "debug" Новый уровень логирования.
durationMs number (positive int) 600000 (10 мин) Длительность до auto-revert. Максимум 1800000 (30 мин).

Response

200 OK:

{
  "level":    "debug",
  "previous": "info",
  "revertAt": "2026-05-29T10:25:00.000Z",
  "message":  "Log level changed to 'debug' for 600s. Was: 'info'."
}

Errors

Код Условие
401 Отсутствует/невалидный Authorization: Bearer
422 durationMs > 1800000 или невалидный level

curl

curl -sS -X POST \
  -H "Authorization: Bearer $ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"level":"debug","durationMs":600000}' \
  http://localhost:3000/admin/debug-level

Ссылки


4. POST /admin/fee-rules/dry-run

Файл: src/admin/fee-rules.ts:24.

Назначение

Безопасно вычисляет fee-выражение (JavaScript-выражение из rule-engine) на тестовом контексте — без сохранения в БД. Используется админ-панелью для preview правил перед публикацией.

Логика:

  1. Парсинг и валидация тела через Zod.
  2. Вызов evalFeeExpression(expression, context) из rule-engine — sandboxed eval.
  3. Суммирование splits[].amount (bigint) в totalFee.
  4. Сериализация bigint-сумм в строки (для JSON-безопасности).

Auth

Authorization: Bearer <ADMIN_SECRET>

Request

POST /admin/fee-rules/dry-run
Content-Type: application/json

{
  "expression": "[{ account: 'fee_pool', amount: BigInt(Math.floor(amount * 0.01)) }]",
  "context": {
    "operationType": "P2P_TRANSFER",
    "amount":        100000,
    "currency":      "THB",
    "metadata":      {}
  }
}
Поле Тип Описание
expression string (min 1) JS-выражение, возвращающее массив { account, amount }.
context.operationType string (min 1) Тип операции — доступен в выражении как operationType.
context.amount number (positive int) Сумма в satang.
context.currency string (length 3) ISO-4217 код.
context.metadata Record<string, unknown> (optional) Произвольные поля контекста.

Response

200 OK:

{
  "splits": [
    { "account": "fee_pool", "amount": "1000" }
  ],
  "totalFee": "1000"
}
  • splits[].amount и totalFeeстроки (bigint сериализуется в строку).

Errors

Код error Условие
401 UNAUTHORIZED Невалидный bearer-токен
422 VALIDATION_ERROR Невалидное тело запроса (Zod)
422 EVAL_ERROR Выражение бросило исключение или вернуло неверную форму

curl

curl -sS -X POST \
  -H "Authorization: Bearer $ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "expression": "[{ account: \"fee_pool\", amount: BigInt(Math.floor(amount * 0.01)) }]",
    "context": {
      "operationType": "P2P_TRANSFER",
      "amount":        100000,
      "currency":      "THB"
    }
  }' \
  http://localhost:3000/admin/fee-rules/dry-run

Ссылки


5. POST /admin/intents/:id/resolve

Файл: src/admin/resolve-intent.ts:26.

Назначение

Принудительно завершает intent, застрявший в статусе MANUAL_REVIEW (например, после ошибки PSP или сработавшего risk-rule). Сервис должен иметь permission forceResolve = true в pm.service_key.permissions.

Путь именно такой: POST /admin/intents/:id/resolve — НЕ /admin/resolve-intent. :id — UUID intent-а.

Поведение по resolution:

resolution Действия
CONFIRMED Вставляется outbox_event(action='post_pending'). Сам intent остаётся в MANUAL_REVIEWOutboxWorker асинхронно переведёт его в SETTLED (post pending в TigerBeetle).
FAILED В одной транзакции: intent.status = FAILED, failureReason = reason ?? 'Manually resolved as failed', updatedAt = now(), плюс outbox_event(action='void_pending') для очистки pending-трансферов в TB.

Идемпотентность: если intent уже в SETTLED или FAILED, endpoint возвращает 200 с previousStatus = <текущий статус> — без побочных эффектов.

Auth

Authorization: Bearer <ADMIN_SECRET>

Дополнительно: сервис, чей serviceId указан в HMAC-заголовках вызова, должен иметь permissions.forceResolve = true в таблице pm.service_key (см. docs/dev/modules/auth.md).

Request

POST /admin/intents/0c4f3a18-9d52-4a3a-b6e1-1a2c5b9e1234/resolve
Content-Type: application/json

{
  "resolution": "CONFIRMED",
  "reason":     "Risk-team approved manually"
}
Поле Тип Описание
id (path) string (UUID) Идентификатор intent-а.
resolution "CONFIRMED" \| "FAILED" Итоговое разрешение.
reason string (max 255, optional) Сохраняется в intent.failure_reason для FAILED. Игнорируется для CONFIRMED.

Response

201 Created — разрешение применено:

{
  "intentId":       "0c4f3a18-9d52-4a3a-b6e1-1a2c5b9e1234",
  "resolution":     "CONFIRMED",
  "previousStatus": "MANUAL_REVIEW"
}

200 OK — идемпотентный повтор (intent уже SETTLED/FAILED):

{
  "intentId":       "0c4f3a18-9d52-4a3a-b6e1-1a2c5b9e1234",
  "resolution":     "CONFIRMED",
  "previousStatus": "SETTLED"
}

Errors

Код error Условие
401 UNAUTHORIZED Невалидный bearer-токен
403 FORBIDDEN Сервис не имеет forceResolve permission
404 NOT_FOUND Intent с указанным id не найден
422 WRONG_STATUS Intent в статусе, отличном от MANUAL_REVIEW/SETTLED/FAILED

curl

curl -sS -X POST \
  -H "Authorization: Bearer $ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"resolution":"CONFIRMED"}' \
  http://localhost:3000/admin/intents/0c4f3a18-9d52-4a3a-b6e1-1a2c5b9e1234/resolve
# FAILED с reason
curl -sS -X POST \
  -H "Authorization: Bearer $ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"resolution":"FAILED","reason":"PSP timeout — manually voided"}' \
  http://localhost:3000/admin/intents/0c4f3a18-9d52-4a3a-b6e1-1a2c5b9e1234/resolve

Ссылки


Связанные документы