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

06 — Service Keys (Реестр сервисных ключей)

Канонический реестр HMAC service keys, которым разрешено вызывать Payment Manager: их назначение, список allowedOperationTypes, особые permissions и источник секрета.

Назначение

Payment Manager не имеет «доверенных» вызовов — любой запрос на write-эндпоинты (POST /intents, POST /accounts/*, и др.) обязан быть подписан HMAC-SHA256. Каждому внешнему клиенту PM соответствует service key — запись в таблице pm.service_key с уникальным service_id и JSON-объектом permissions (DDL и индексы описаны в ../database/11-service-key.md).

Permissions определяют:

  • allowedOperationTypes — какие operationType разрешено создавать этим ключом;
  • allowToTbAccountId — можно ли передавать сырой TigerBeetle account id вместо имени;
  • forceResolve — может ли ключ принудительно ресолвить системные/закрытые аккаунты;
  • fromAccountOverride / toAccountOverride — какие типы и шаблоны имён аккаунтов допустимы в from / to (защита от инъекции системных аккаунтов).

Сами секреты в БД не хранятся — только реестр и permissions. Секреты подгружаются из env SERVICE_SECRETS (см. ниже).

HMAC-заголовки

Каждый защищённый запрос обязан содержать три заголовка:

Заголовок Что внутри
X-Service-Id имя ключа (auth-center, auth-center-merchant, ...) — должно быть в SERVICE_SECRETS
X-Timestamp UNIX seconds, текущий момент клиента
X-Signature hex-HMAC-SHA256 от signing string (см. ниже)

Signing string — четыре строки, разделённые \n:

${timestamp}\n${METHOD}\n${PATH}\n${sha256hex(rawBody)}

где:

  • METHODPOST / GET в верхнем регистре;
  • PATHrequest.url (полный путь + query), без хоста;
  • sha256hex(rawBody) — hex-дайджест raw body (для GET — пустой строки).

Replay window: ±60 секунд относительно server time. При большем drift запрос отклоняется с UnauthorizedError. См. src/auth/hmac.ts и src/auth/hmacPlugin.ts.

Реестр service keys

Таблица отражает текущий seed (см. drizzle/seed.ts). Расширения (например, exchange-webhook) активируются опционально, если соответствующий секрет присутствует в SERVICE_SECRETS.

service_id Кто это allowedOperationTypes Особые permissions
auth-center Основной клиент — Serverpod Auth Center, создаёт большинство user-driven intents P2P_TRANSFER, IPPS_WITHDRAWAL, THAI_QR_PAY, WITHDRAWAL, QP_TOPUP, MINIAPP_CHARGE, MINIAPP_CREDIT, INVOICE_PAYMENT allowToTbAccountId: true; from/toAccountOverride ограничены типами USER_WALLET / MERCHANT_WALLET / AGENT_WALLET и namePattern ^(user\|merchant\|agent)\.
auth-center-merchant Отдельный ключ для NFC-pull-charge сценария от мерчанта (терминал инициирует списание с кошелька клиента) Только NFC_CHARGE allowToTbAccountId: false; fromAccountOverrideUSER_WALLET / AGENT_WALLET (^(user\|agent)\.); toAccountOverride — только MERCHANT_WALLET (^merchant\.). Направление списания фиксируется permissions, не доверием.
nginx-gateway Шлюз nginx → PM для read-only маршрутов (/api/pm/accounts/balance, history); intents не создаёт [] (пусто) Нет
admin-tool Внутренний CLI/utility инструмент оператора [] forceResolve: true — может ресолвить системные/закрытые аккаунты
admin-panel SvelteKit Admin Panel, выполняет ручные корректировки/трансферы со стороны бухгалтерии ADMIN_TRANSFER (канал — ADMIN) Очень широкие from/toAccountOverride: типы EQUITY / NOSTRO / REVENUE / USER_WALLET / TRANSIT / SERVICE_ACCOUNT / MERCHANT_WALLET / MERCHANT_SETTLEMENT / AGENT_WALLET / AGENT_SETTLEMENT; namePattern ^(system\|user\|merchant\|agent\|service)\.
exchange-webhook (опц.) Внешний exchange webhook receiver — поднимается seed-ом, только если в SERVICE_SECRETS присутствует ключ exchange-webhook SERVICE_DEPOSIT fromAccountOverrideSERVICE_ACCOUNT с namePattern ^service\.9001\..*$ (жёстко привязан к одному service account)

Важное напоминание про auth-center-merchant: ключ существует именно для того, чтобы у Auth Center был отдельный, узко-ограниченный credential для NFC-сценария. Permissions запрещают ему создавать что-либо кроме NFC_CHARGE и направлять платёж куда-либо кроме merchant-кошелька. Расширять список allowedOperationTypes у этого ключа нельзя — заводите новый ключ.

Permissions: краткий справочник

Поле Тип Смысл
allowedOperationTypes string[] Белый список. Пустой массив = ключ не имеет права создавать intents (только read-only/служебные операции).
allowToTbAccountId boolean Если true, в DTO можно передавать сырой TigerBeetle account id (tbAccountId) вместо имени. Используется только auth-center.
forceResolve boolean Снимает ограничения на ресолв «закрытых»/системных аккаунтов. Только admin-tool.
fromAccountOverride.allowedTypes string[] Разрешённые account_type для from-аккаунта.
fromAccountOverride.namePattern regexp Дополнительная проверка имени from-аккаунта.
toAccountOverride.* как у from То же самое для to-аккаунта.

Эти поля валидируются в HMAC/intent-pipeline до того, как intent попадает в saga.

Откуда берётся секрет

Секреты не хранятся в БД. Они подгружаются из env-переменной SERVICE_SECRETS — JSON-объекта вида:

{
  "auth-center":          "<random-32+chars>",
  "auth-center-merchant": "<random-32+chars>",
  "nginx-gateway":        "<random-32+chars>",
  "admin-tool":           "<random-32+chars>",
  "admin-panel":          "<random-32+chars>",
  "exchange-webhook":     "<random-32+chars>"
}

Схема env описана в src/shared/config.ts (SERVICE_SECRETS: JSON.parse(...)). PM при загрузке:

  1. Парсит JSON.
  2. На каждый входящий HMAC-запрос ищет секрет по X-Service-Id в этой map (src/auth/hmacPlugin.ts).
  3. Если service_id отсутствует в SERVICE_SECRETS — запрос отклоняется (Unknown service), даже если запись pm.service_key для него есть.

drizzle/seed.ts дополнительно выполняет deploy-time guard: падает, если в env нет секретов для основных ключей (auth-center, nginx-gateway, admin-tool, admin-panel, auth-center-merchant). Это гарантирует, что seed синхронен с runtime.

NO-GO: не коммитить SERVICE_SECRETS в репозиторий, не хранить в БД, не логировать. В production значение приходит из vault/secret manager.

Как заводится новый service key

Полный пошаговый чеклист см. в ../../cookbook/add-service-key.md (TODO — будет создан в Phase 6).

Краткий порядок действий:

  1. Сгенерировать секрет (≥32 байт энтропии), добавить пару serviceId → secret в SERVICE_SECRETS во всех окружениях.
  2. Добавить запись в drizzle/seed.ts с минимальным набором allowedOperationTypes и максимально узкими from/toAccountOverride.
  3. Прогнать seed (npm run db:seed).
  4. Поделиться секретом с клиентом по защищённому каналу.
  5. Проверить, что клиент корректно строит signing string и попадает в ±60s replay window.

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