Admin API¶
Административные endpoint-ы Payment Manager. Используются для health-check Kubernetes-проб, runtime-управления логированием, отладки fee-правил и принудительного разрешения зависших intent-ов.
Аутентификация admin endpoint-ов¶
Все /admin/* маршруты защищены плагином adminPlugin.ts, который выполняет проверку bearer-токена в preHandler:
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 объединяет три проверки:
- Event loop pressure — через плагин
@fastify/under-pressure, зарегистрированный вsrc/server.ts:84сmaxEventLoopDelay: 1000. Если задержка event-loop превышает 1000 мс —app.isUnderPressure()вернётtrue. - TigerBeetle reachability — выполняется
getTb().lookupAccounts([])(пустой lookup как ping). Если SDK недоступен или TB-соединение оборвано — 503. - Базовая жизнеспособность процесса — если Fastify отвечает, процесс жив.
В проекте нет разделения на
/healthz,/readyz,/startupz,/livez. Используется только/health.
Auth¶
Не требуется. Открытый endpoint.
Request¶
Без параметров, без тела.
Response¶
200 OK — сервис здоров:
version— изprocess.env.npm_package_version;"0.0.0"если не задана.timestamp— ISO-8601 момента обработки запроса.
503 Service Unavailable — сервис деградирован:
Возможные значения message:
Service under pressure— превышенmaxEventLoopDelay(1000 мс).TigerBeetle unavailable—lookupAccountsбросил исключение.
Errors¶
| Код | Условие |
|---|---|
| 503 | Event loop > 1000 мс или TigerBeetle недоступен |
curl¶
Ссылки¶
- Реализация:
src/admin/health.ts - Регистрация плагина under-pressure:
src/server.ts:84
2. GET /admin/debug-level¶
Файл: src/admin/debug-level.ts:63.
Назначение¶
Возвращает текущий уровень логирования pino-логгера и (если активирован debug-режим) время автоматического возврата к info.
Используется как контроль перед/после POST /admin/debug-level.
Auth¶
Request¶
Без параметров, без тела.
Response¶
200 OK:
level— один из"debug" | "info" | "warn" | "error".revertAt— ISO-8601 момента, когда уровень автоматически вернётся кinfo.null, если активен дефолтныйinfo-уровень (никаких таймеров не запущено).
Errors¶
| Код | Условие |
|---|---|
| 401 | Отсутствует/невалидный Authorization: Bearer |
curl¶
Ссылки¶
- Реализация:
src/admin/debug-level.ts
3. POST /admin/debug-level¶
Файл: src/admin/debug-level.ts:30.
Назначение¶
Временно меняет уровень логирования pino runtime — без рестарта сервиса. По истечении durationMs уровень автоматически возвращается к info (через setTimeout).
Используется для диагностики продакшен-инцидентов: повышаем до debug на 10 минут, собираем трассы, ждём auto-revert.
Auth¶
Request¶
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
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
Ссылки¶
- Реализация:
src/admin/debug-level.ts - Логгер:
src/shared/logger.ts
4. POST /admin/fee-rules/dry-run¶
Файл: src/admin/fee-rules.ts:24.
Назначение¶
Безопасно вычисляет fee-выражение (JavaScript-выражение из rule-engine) на тестовом контексте — без сохранения в БД. Используется админ-панелью для preview правил перед публикацией.
Логика:
- Парсинг и валидация тела через Zod.
- Вызов
evalFeeExpression(expression, context)изrule-engine— sandboxed eval. - Суммирование
splits[].amount(bigint) вtotalFee. - Сериализация bigint-сумм в строки (для JSON-безопасности).
Auth¶
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[].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
Ссылки¶
- Реализация:
src/admin/fee-rules.ts - Sandbox-эвалуатор:
src/rule-engine/evaluator.ts
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_REVIEW — OutboxWorker асинхронно переведёт его в 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¶
Дополнительно: сервис, чей 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
Ссылки¶
- Реализация:
src/admin/resolve-intent.ts - OutboxWorker:
docs/dev/modules/workers.md - Канал
ADMIN:docs/dev/modules/channels.md