11 service key
Таблица pm.service_key — реестр сервисов, которые могут стучаться в Payment Manager по HMAC: какие operationType разрешены, какие override-маски для from/to аккаунтов, разрешено ли передавать TB-account-id напрямую.
Имя¶
pm.service_key
Назначение¶
service_key отвечает на единственный вопрос про входящий HMAC-запрос: «что этому serviceId разрешено делать?». Сам секрет ключа в БД не хранится (см. ниже про колонку secret_hash и миграцию 0005) — он живёт исключительно в env SERVICE_SECRETS ({serviceId: secret}). БД хранит только permissions — то, как PM ограничивает запрос после успешной проверки подписи.
Конкретно permissions управляет:
- какие
operationTypeразрешены для сервиса (allowedOperationTypes); - какие маски аккаунтов допустимы для
fromAccountName/toAccountName(fromAccountOverride/toAccountOverride—allowedTypes+namePattern); - можно ли подменять TB-account-id напрямую в payload (
allowToTbAccountId); -
разрешено ли force-resolve имени аккаунта (
forceResolve— для admin-tool). -
Пишут: только seed-скрипт
drizzle/seed.ts— upsert поserviceId. Через миграции тоже допустимо засеять/обновить строки (см.0006_rapid_millenium_guard.sql). - Читают:
src/auth/hmacPlugin.tsиsrc/intent/context-builder.ts— для разрешения сервиса и enforce-а его permissions при построении контекста интента.
DDL¶
История миграций для таблицы:
| Миграция | Что делает |
|---|---|
0000_init.sql |
Создаёт таблицу в первоначальном виде — с колонкой secret_hash text NOT NULL (хранение хеша HMAC-секрета). |
0005_skinny_skreet.sql (см. drizzle/migrations/0005_skinny_skreet.sql) |
ALTER TABLE pm.service_key DROP COLUMN secret_hash; — после версии 0.2.0 секрет уехал в env SERVICE_SECRETS, БД больше не «знает» хеши. |
0006_rapid_millenium_guard.sql |
Seed-вставка ключа auth-center-merchant (см. ниже про permissions этой строки). |
Текущая (финальная) схема:
-- 0000_init.sql (исходно)
CREATE TABLE "pm"."service_key" (
"service_id" varchar(50) PRIMARY KEY NOT NULL,
"secret_hash" text NOT NULL, -- удалено в 0005
"permissions" jsonb NOT NULL,
"active" boolean DEFAULT true NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
-- 0005_skinny_skreet.sql
ALTER TABLE "pm"."service_key" DROP COLUMN "secret_hash";
Drizzle-описание — src/shared/schema.ts (строки 43-51, тип ServiceKey, интерфейс ServicePermissions).
Поля¶
| Колонка | Тип | NULL | Default | Описание |
|---|---|---|---|---|
service_id |
varchar(50) |
NO | — | PK. Логическое имя сервиса; матчится с HTTP-заголовком X-Service-Id. Примеры: auth-center, nginx-gateway, admin-tool, admin-panel, auth-center-merchant, exchange-webhook. |
permissions |
jsonb |
NO | — | Структура ServicePermissions (см. ниже). Описывает, что сервис вправе делать после успешной HMAC-проверки. |
active |
boolean |
NO | true |
Soft-disable: если false, ключ существует, но не должен использоваться. На текущий момент PM проверяет наличие service_id через resolve в src/intent/context-builder.ts; фильтр active=false — задел на будущую логику. |
created_at |
timestamptz |
NO | now() |
Когда строка появилась (через seed или миграцию). |
Колонки
secret_hashв таблице больше нет (удалена в 0005). Секрет читается из envSERVICE_SECRETS— JSON-объект{ "auth-center": "...", "auth-center-merchant": "...", ... }, парсится один раз вsrc/shared/config.tsи проверяется вsrc/auth/hmacPlugin.ts.
Структура permissions (jsonb)¶
Тип ServicePermissions — src/shared/schema.ts строки 34-41:
| Ключ | Тип | Семантика |
|---|---|---|
allowedOperationTypes |
string[] |
Whitelist operationType интентов, которые сервис может создавать. Пустой массив = сервису запрещены write-операции через POST /intents (используется для nginx-gateway, которому нужны только read-эндпоинты). |
merchantId |
number? |
Опциональный merchantId, к которому привязан ключ (используется для merchant-specific интеграций). |
forceResolve |
boolean? |
Если true, PM при resolve-е имени аккаунта не падает, если строки нет в tb_account_map, а пытается создать / докинуть данные. Применимо только к admin-операциям; в seed выставлено у admin-tool. |
allowToTbAccountId |
boolean? |
Если true, сервис вправе передавать toTbAccountId в payload напрямую (минуя resolve по account_name). Это нужно auth-center для прямых переводов на known TB-account; для auth-center-merchant намеренно выключено (false). |
fromAccountOverride |
AccountOverrideRule? |
Маска для fromAccountName: { allowedTypes?: AccountType[], namePattern?: string }. Если задано — from обязан и совпасть с одним из allowedTypes (см. 06-tb-account-map.md), и matched регулярному выражению namePattern. |
toAccountOverride |
AccountOverrideRule? |
То же для toAccountName. |
Если fromAccountOverride / toAccountOverride не заданы — PM применяет дефолтные правила resolve по operationType (см. src/intent/context-builder.ts и dev/reference/passport/06-service-keys.md — TODO).
Индексы¶
| Индекс | Тип | Колонки | Назначение |
|---|---|---|---|
service_key_pkey |
btree, PK | service_id |
Lookup по X-Service-Id при HMAC-проверке и при построении контекста интента. |
Дополнительных индексов нет — таблица маленькая (единицы строк), все запросы идут по PK.
Связи¶
Логические (без FK)¶
| Связь | Куда | Колонка |
|---|---|---|
1 : N |
pm.intent — все интенты, созданные этим сервисом |
service_key.service_id → intent.service_id |
FK не объявлен — intent.service_id хранится как самостоятельное значение, чтобы строка интента не ломалась при ротации/удалении ключа сервиса.
Внешний контракт: HMAC-заголовки¶
Сервис, обращающийся к PM, должен прислать три заголовка (см. src/auth/hmac.ts и src/auth/hmacPlugin.ts):
| Заголовок | Что |
|---|---|
X-Service-Id |
serviceId (PK таблицы). |
X-Timestamp |
Unix-секунды; PM отвергает запрос с дрейфом более 60 секунд. |
X-Signature |
hex(HMAC-SHA256(secret, signingString)), где signingString = ${timestamp}\n${METHOD}\n${PATH}\n${sha256hex(rawBody)}. |
secret берётся из config.SERVICE_SECRETS[serviceId] (env SERVICE_SECRETS). Если для serviceId нет секрета в env — PM возвращает 401 Unknown service, даже если строка с этим service_id есть в БД (и наоборот: секрет без строки в БД не даст пройти permissions-проверку при создании интента).
Связанный код¶
| Модуль / функция | Роль |
|---|---|
src/shared/schema.ts |
Drizzle-таблица serviceKey, типы ServiceKey, NewServiceKey, ServicePermissions, AccountOverrideRule. |
src/auth/hmacPlugin.ts |
Fastify-плагин: читает X-Service-Id / X-Timestamp / X-Signature, ищет секрет в env, вызывает verifyHmacRequest. Регистрируется на роуты POST /intents, POST /accounts и т. п. |
src/auth/hmac.ts |
signRequest() / verifyHmacRequest() / sha256hex() — формат подписи. |
src/intent/context-builder.ts |
Читает service_key по serviceId, применяет permissions (allowedOperationTypes, fromAccountOverride, toAccountOverride, allowToTbAccountId) при построении контекста интента. |
drizzle/seed.ts |
Сидит набор сервисов (auth-center, nginx-gateway, admin-tool, admin-panel, auth-center-merchant, опционально exchange-webhook). На старте проверяет, что для каждого есть secret в env, иначе падает с понятной ошибкой. |
dev/reference/passport/06-service-keys.md |
Канонический контракт сервис-ключей: их назначение, разрешения и приёмники (TODO). |
Примеры запросов и seed-строки¶
Список активных сервис-ключей с их allowedOperationTypes¶
SELECT service_id,
permissions->>'allowedOperationTypes' AS allowed_ops,
active
FROM pm.service_key
ORDER BY service_id;
Кто вправе делать NFC_CHARGE¶
SELECT service_id
FROM pm.service_key
WHERE active = true
AND permissions->'allowedOperationTypes' ? 'NFC_CHARGE';
Ожидаемый результат: ровно один сервис — auth-center-merchant. Любые другие сервисы с NFC_CHARGE — багрепорт.
Seed-строка: auth-center-merchant (NFC_CHARGE only)¶
Из drizzle/seed.ts (строки ~97-115; миграция-инициализатор — 0006_rapid_millenium_guard.sql):
{
"allowedOperationTypes": ["NFC_CHARGE"], // только это, ничего больше
"allowToTbAccountId": false, // запрещаем передавать tbAccountId напрямую
"fromAccountOverride": { // тянем pull-charge: user/agent → merchant
"allowedTypes": ["USER_WALLET", "AGENT_WALLET"],
"namePattern": "^(user\\.|agent\\.)"
},
"toAccountOverride": {
"allowedTypes": ["MERCHANT_WALLET"],
"namePattern": "^merchant\\."
}
}
Двойная защита allowedTypes + namePattern сделана сознательно: allowedTypes — основной guard (валидация по tb_account_map.account_type), namePattern — дополнительный фильтр на форму имени аккаунта (защита от опечатки / залётной системной записи).
Seed-строка: admin-panel (ADMIN-канал)¶
{
"allowedOperationTypes": ["ADMIN_TRANSFER"],
"fromAccountOverride": { "allowedTypes": ["EQUITY", "NOSTRO", "REVENUE", "USER_WALLET", "TRANSIT", "SERVICE_ACCOUNT", "MERCHANT_WALLET", "MERCHANT_SETTLEMENT", "AGENT_WALLET", "AGENT_SETTLEMENT"], "namePattern": "^(system\\.|user\\.|merchant\\.|agent\\.|service\\.)" },
"toAccountOverride": { /* симметрично */ }
}
ADMIN_TRANSFER маршрутизируется в канал ADMIN (см. drizzle/seed.ts, seed paymentRoute).
Disable сервис-ключа (operational)¶
(Реальный отзыв доступа — это удалить запись из env SERVICE_SECRETS и перезапустить PM; active=false — это soft-flag на будущее.)