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:
где:
METHOD—POST/GETв верхнем регистре;PATH—request.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; fromAccountOverride — USER_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 |
fromAccountOverride — SERVICE_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 при загрузке:
- Парсит JSON.
- На каждый входящий HMAC-запрос ищет секрет по
X-Service-Idв этой map (src/auth/hmacPlugin.ts). - Если
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).
Краткий порядок действий:
- Сгенерировать секрет (≥32 байт энтропии), добавить пару
serviceId → secretвSERVICE_SECRETSво всех окружениях. - Добавить запись в
drizzle/seed.tsс минимальным наборомallowedOperationTypesи максимально узкимиfrom/toAccountOverride. - Прогнать seed (
npm run db:seed). - Поделиться секретом с клиентом по защищённому каналу.
- Проверить, что клиент корректно строит signing string и попадает в ±60s replay window.
Связанные документы¶
../database/11-service-key.md— DDL таблицыpm.service_key, индексы, типы permissions.03-operation-types.md— полный списокoperationType(что вообще можно класть вallowedOperationTypes).02-channels.md— какие channels соответствуют каким operationType (включаяADMIN,INTERNAL_P2P,MERCHANT_INVOICE).../../cookbook/add-service-key.md— практический how-to (TODO).../../../../src/auth/hmac.ts,../../../../src/auth/hmacPlugin.ts— реализация HMAC-проверки.