Cookbook: Добавить новый service-key¶
Как добавить новый HMAC-ключ внешнего сервиса в PM с ограниченным набором операций.
Цель¶
Добавить новый serviceId в таблицу pm.service_key, чтобы внешний сервис мог вызывать PM-endpoints (POST /intents, /intents/quote, …) через HMAC-подпись. Ключ должен иметь минимально достаточные права: список allowedOperationTypes и (опционально) маски от/до-счетов.
Пример в этом гайде — auth-center-merchant: ключ Auth Center'а, которому разрешён только NFC_CHARGE (списание с user/agent wallet в merchant wallet).
Предусловия¶
- Локальный PM запущен, есть доступ к
pm.*в Postgres. - Понимаешь, какие
operationTypeнужны ключу — см.../reference/passport/03-operation-types.md. - Понимаешь, какие
AccountTypeучаствуют — см.../reference/database/06-tb-account-map.md. - У сервиса-клиента есть возможность хранить HMAC-секрет в своём env (PM сам секреты в БД не хранит).
Как это работает¶
PM проверяет HMAC-подпись каждого входящего запроса в плагине src/auth/hmacPlugin.ts:
- Из заголовка
X-Service-IdберётсяserviceId. - Секрет для этого
serviceIdищется в env-переменнойSERVICE_SECRETS(одна общая JSON-карта{ "<service-id>": "<base64-secret>", … }). - Проверяется подпись (
src/auth/hmac.ts:verifyHmacRequest) —HMAC-SHA256(secret, "ts\nMETHOD\npath\nsha256(body)"), timestamp-drift ≤ 60 секунд. - После успешной проверки HMAC из
pm.service_keyподгружаютсяpermissionsдля этогоserviceId(active=true). Дальше PM по этимpermissionsрешает, можно ли выполнять запрошенныйoperationTypeи какие счета допустимы.
Ключевая идея: секрет — в env, права — в БД. Поэтому добавление нового ключа — это всегда две правки: env (для подписи) и миграция (для прав).
Шаги¶
1. Сгенерировать секрет¶
Сохрани значение — оно понадобится и PM, и сервису-клиенту. Минимум 32 байта энтропии; чаще используем 48 байт base64.
2. Добавить секрет в SERVICE_SECRETS¶
В .env PM (и в env сервиса-клиента, если он сам подписывает запросы) SERVICE_SECRETS — это один JSON-объект со всеми ключами:
# .env (PM)
SERVICE_SECRETS='{
"auth-center": "…",
"nginx-gateway": "…",
"admin-tool": "…",
"admin-panel": "…",
"auth-center-merchant": "<новый base64-секрет>"
}'
Никаких
SERVICE_KEY_SECRET_AUTH_CENTERили per-service env-переменных — структура одна, см.src/shared/config.ts(envSchema.SERVICE_SECRETSпарсится черезJSON.parse).
В drizzle/seed.ts есть deploy-time guard: если в SERVICE_SECRETS не оказалось одного из обязательных ключей — seed упадёт. При добавлении нового обязательного ключа обнови этот список (см. шаг 4).
3. Создать миграцию drizzle/migrations/NNNN_<name>_service_key.sql¶
Следующий свободный номер — 0010_…, 0011_… и т.д. (посмотри drizzle/migrations/). Имя — kebab-case, короткое и по делу: 0010_add_auth_center_merchant_service_key.sql.
-- 0010_add_auth_center_merchant_service_key.sql
--
-- auth-center-merchant: NFC_CHARGE only.
-- Списание с USER_WALLET / AGENT_WALLET в MERCHANT_WALLET через INTERNAL_P2P.
-- Секрет НЕ хранится в БД — только в env SERVICE_SECRETS.
INSERT INTO pm.service_key (service_id, permissions, active)
VALUES (
'auth-center-merchant',
'{
"allowedOperationTypes": ["NFC_CHARGE"],
"allowToTbAccountId": false,
"fromAccountOverride": {
"allowedTypes": ["USER_WALLET", "AGENT_WALLET"],
"namePattern": "^(user\\.|agent\\.)"
},
"toAccountOverride": {
"allowedTypes": ["MERCHANT_WALLET"],
"namePattern": "^merchant\\."
}
}'::jsonb,
true
)
ON CONFLICT (service_id) DO UPDATE SET
permissions = EXCLUDED.permissions,
active = true;
Поля permissions (см. src/shared/schema.ts:ServicePermissions):
| Поле | Назначение |
|---|---|
allowedOperationTypes |
Белый список operationType для этого ключа. Пусто = ничего нельзя инициировать (только read-only). |
merchantId |
Жёстко привязывает ключ к одному merchantId (используется, например, для интеграций конкретного мерчанта). |
forceResolve |
Разрешает PM авто-резолвить from/to по userId, минуя явное указание счёта. Опасно — давай только админ-ключам. |
fromAccountOverride / toAccountOverride |
Маски на AccountType и имя счёта. Первая линия защиты — allowedTypes, namePattern лишь страхует форму имени. |
allowToTbAccountId |
Разрешает использовать сырой TB account id в toAccount (обычно нужно auth-center, чтобы платить по system.* счетам). |
Чтобы по-русски подсветить зачем именно так — оставь короткие комментарии прямо в SQL (см. seed.ts:auth-center-merchant, namePattern — это вторичный guard для shape-имени, основная защита — allowedTypes).
Канал —
ADMIN(для админских корректировок) илиINTERNAL_P2P(для пуш-charge), но неADMIN_TRANSFER: этоoperationType, а не канал. Каналы — в../reference/passport/02-channels.md.
4. Обновить drizzle/seed.ts (если ключ "обязательный")¶
Если новый ключ должен присутствовать в любой dev/staging/prod-инсталляции — добавь его и в seed.ts, чтобы локальный npm run db:seed создавал ту же запись и чтобы guard в начале seed'а отлавливал отсутствие секрета:
// seed.ts — фрагмент seeds[]
{
serviceId: 'auth-center-merchant',
permissions: {
allowedOperationTypes: ['NFC_CHARGE'],
allowToTbAccountId: false,
fromAccountOverride: {
allowedTypes: ['USER_WALLET', 'AGENT_WALLET'],
namePattern: '^(user\\.|agent\\.)',
},
toAccountOverride: {
allowedTypes: ['MERCHANT_WALLET'],
namePattern: '^merchant\\.',
},
},
active: true,
},
И в guard'е if (!secrets['auth-center'] || …) — добавь проверку !secrets['auth-center-merchant'].
Если ключ нужен только в одной среде (например, временная интеграция) — достаточно миграции, в
seed.tsего добавлять не нужно. Аналогичноexchange-webhookв seed'е — он создаётся только если секрет реально присутствует в env.
5. Применить миграцию¶
Проверить запись:
SELECT service_id, permissions, active
FROM pm.service_key
WHERE service_id = 'auth-center-merchant';
6. Обновить документацию¶
../reference/passport/06-service-keys.md— добавить строку про новыйserviceId, его разрешения и канал использования.../reference/database/11-service-key.md— обновлять только если изменилась схема таблицы или структураServicePermissions(новое поле и т.п.). Чисто новая строка-данные сюда не идёт.
7. Обновить CHANGELOG.md¶
В корне PM:
## [Unreleased]
### Added
- service_key: добавлен `auth-center-merchant` (NFC_CHARGE only, USER/AGENT → MERCHANT).
Проверка вручную (smoke-test)¶
- Сервис-клиент берёт секрет, генерирует подпись по
signRequest(secret, ts, 'POST', '/intents/quote', sha256hex(body))(см.src/auth/hmac.ts:signRequest). - Шлёт
POST /intents/quoteс заголовками: X-Service-Id: auth-center-merchantX-Timestamp: <unix-sec>X-Signature: <hex>- Ожидаемый ответ —
200дляoperationType: 'NFC_CHARGE';403(илиINSUFFICIENT_PRIVILEGE) для любого другогоoperationType, напримерP2P_TRANSFER. - Если
from.accountначинается сsystem.…— PM должен отказать (маска^(user\.|agent\.)запрещает).
Откат¶
Чтобы временно вырубить ключ без миграции — обнови строку в БД:
Полное удаление — отдельной миграцией (DELETE FROM pm.service_key WHERE …), и только после ротации секрета в env у всех клиентов.
Связанные документы¶
../reference/passport/06-service-keys.md— канонический список всехserviceIdи их прав.../reference/database/11-service-key.md— схема таблицыpm.service_key.../reference/passport/02-channels.md— список каналов (INTERNAL_P2P,ADMIN,IPPS_TRANSFER, …).../reference/passport/03-operation-types.md— списокoperationType.src/auth/hmac.ts,src/auth/hmacPlugin.ts— реализация HMAC-проверки.src/shared/config.ts— env-схема (SERVICE_SECRETS).drizzle/seed.ts— образцы seed-конфигов ключей.