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

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 / toAccountOverrideallowedTypes + 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). Секрет читается из env SERVICE_SECRETS — JSON-объект { "auth-center": "...", "auth-center-merchant": "...", ... }, парсится один раз в src/shared/config.ts и проверяется в src/auth/hmacPlugin.ts.

Структура permissions (jsonb)

Тип ServicePermissionssrc/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_idintent.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)

UPDATE pm.service_key
SET    active = false
WHERE  service_id = $1;

(Реальный отзыв доступа — это удалить запись из env SERVICE_SECRETS и перезапустить PM; active=false — это soft-flag на будущее.)

Полный дамп permissions конкретного сервиса (диагностика)

SELECT jsonb_pretty(permissions)
FROM   pm.service_key
WHERE  service_id = 'auth-center-merchant';