Cookbook: Написать новую auth-policy (step-up правило)¶
Краткое руководство по созданию нового правила step-up аутентификации в таблице pm.auth_policies. Правило определяет, какой уровень подтверждения (PIN, BIOMETRIC, OTP, KYC_UPLIFT) требуется от пользователя для конкретного платёжного сценария.
Перед чтением рекомендуется ознакомиться со справочником полей в ../../AUTH-POLICIES.md — здесь мы концентрируемся на пошаговом workflow и end-to-end примере.
1. Когда использовать¶
Используйте этот рецепт, если:
- Появилось новое требование compliance / антифрод (например, KYC при дневном обороте > 50 000 THB).
- Нужно подтвердить PIN'ом первый платёж новому мерчанту.
- Нужно установить более строгое правило для отдельного mini-app или конкретного мерчанта.
- Нужно временно отключить step-up для микроплатежей (например, маркетинговая акция).
Не используйте, если:
- Требуется новый уровень step-up (например,
VIDEO_CALL) — это изменение enumStepUpLevelвsrc/shared/schema.ts+ миграция CHECK-constraint, не просто новая строка. - Требуется новый ключ condition (например,
weekly_cumulative_gte) — нужно править matcher вsrc/limits/evaluate-policy.ts, см. §10. - Нужно блокировать платёж полностью — это работа
rule-engine, а не auth-policy. Auth-policy не отказывает, она только повышает требования.
2. Решения, которые надо принять ДО SQL¶
Четыре параметра задаются для каждого правила:
| Параметр | Возможные значения | Где описано |
|---|---|---|
scope |
global / app:<id> / merchant:<id> |
§3 |
condition (JSONB) |
amount_lt, amount_gte, amount_lte, currency, channel, daily_cumulative_gte, new_payee |
§4 |
required_step_up |
NONE / PIN / BIOMETRIC / OTP / KYC_UPLIFT |
§5 |
priority |
integer; меньше = первее (lower-wins) | §6 |
3. Шаг 1 — выбрать scope¶
global ← применяется ко всем платежам, независимо от приложения и мерчанта
app:<appId> ← только платежи из конкретного mini-app (поле PolicyRequest.appId)
merchant:<merchantId> ← только платежи конкретному мерчанту (PolicyRequest.merchantId)
Движок одновременно загружает все три скоупа (global, app:<id>, merchant:<id>) и сортирует все найденные правила по priority. Конкретный скоуп не имеет преимущества перед глобальным — правит только priority.
4. Шаг 2 — выбрать ключи condition¶
Все указанные ключи объединяются AND — правило срабатывает, только если все условия выполняются одновременно.
| Ключ | Тип | Семантика |
|---|---|---|
amount_lt |
number | req.amount < value |
amount_gte |
number | req.amount >= value |
amount_lte |
number | req.amount <= value |
currency |
string | req.currency === value (ISO 4217) |
channel |
string | req.channel === value (INTERNAL_P2P, IPPS_TRANSFER, MERCHANT_INVOICE, ADMIN, ...) |
daily_cumulative_gte |
number | Сумма settled DEBIT-операций пользователя за сегодня (UTC) >= value |
new_payee |
boolean | true — мерчант ранее не получал settled-платежей от этого пользователя. Работает только если в запросе передан merchantId. |
Суммы — в наименьших единицах валюты. Для THB: 1 бат = 100 сатангов → 100 бат = 10000, 5 000 бат = 500000.
5. Шаг 3 — выбрать required_step_up¶
| Уровень | Когда уместен |
|---|---|
NONE |
Микроплатежи, известный получатель, всё прозрачно — UX-friendly. |
PIN |
Стандартное подтверждение (любые «обычные» суммы и новые получатели). |
BIOMETRIC |
Крупные суммы, high-value операции. |
OTP |
Чувствительные операции, требующие out-of-band подтверждения. |
KYC_UPLIFT |
Превышение дневных лимитов — пользователь должен повысить уровень KYC, прежде чем платёж пойдёт. |
CHECK-constraint в БД (auth_policies_step_up_chk) гарантирует, что других значений быть не может — попытка вставить KYC_LITE упадёт на уровне Postgres.
6. Шаг 4 — выбрать priority¶
Правило: lower-wins. Движок сортирует политики по priority ASC и возвращает первое совпавшее — поиск останавливается сразу после match.
priority 1 ← самый строгий, проверяется первым
priority 5
priority 10
...
priority 100 ← default (см. DDL), наименьший приоритет
Если хотите, чтобы строгое правило перекрывало мягкое — дайте ему меньший номер. Пример из seed-данных:
priority 5 → daily_cumulative_gte 5_000_000 → KYC_UPLIFT (самый строгий)
priority 10 → amount_lt 10_000 → NONE
priority 15 → new_payee + amount_gte 10_000 → PIN
priority 20 → 10_000 ≤ amount < 500_000 → PIN
priority 30 → amount_gte 500_000 → BIOMETRIC
Для нового правила выбирайте priority так, чтобы оно встало в нужное место в этой иерархии. Если правило должно перекрывать существующие — берите меньший номер; если быть «последним шансом» — больший.
7. Шаг 5 — миграция или seed¶
Auth-policies живут в pm.* схеме, поэтому только через drizzle-kit. Не редактируйте таблицу руками в production — потеряется аудиторский след.
Вариант A — отдельная миграция¶
Создайте файл drizzle/migrations/00XX_auth_policy_<name>.sql:
-- Пример: PIN при amount > 100 THB для P2P new payee
INSERT INTO pm.auth_policies (scope, condition, required_step_up, reason_code, priority)
VALUES (
'global',
'{"channel": "INTERNAL_P2P", "new_payee": true, "amount_gte": 10000}',
'PIN',
'p2p_new_payee_over_100thb',
12
);
Вариант B — добавить в 0008_auth_policies.sql¶
Допустимо только в момент первичного формирования seed-данных (до production-релиза). После релиза правьте через новые миграции.
8. Шаг 6 — apply¶
Изменения вступают в силу немедленно — evaluatePolicy читает таблицу при каждом вызове, кэша нет.
9. Шаг 7 — verify через POST /policies/evaluate¶
Endpoint защищён HMAC (X-Service-Id, X-Timestamp, X-Signature). Подпись считается над METHOD\nPATH\nTIMESTAMP\nBODY (точная схема — в ../api/auth.md).
Минимальный curl-пример с готовой подписью:
TS=$(date +%s)
BODY='{"userId":1,"merchantId":42,"amount":"15000","currency":"THB","channel":"INTERNAL_P2P","appId":"wallet","operationType":"P2P_TRANSFER"}'
SIG=$(printf 'POST\n/policies/evaluate\n%s\n%s' "$TS" "$BODY" \
| openssl dgst -sha256 -hmac "$PM_HMAC_SECRET" -hex \
| awk '{print $2}')
curl -sS -X POST http://localhost:3000/policies/evaluate \
-H "Content-Type: application/json" \
-H "X-Service-Id: auth-center" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG" \
-d "$BODY"
Ожидаемый ответ для правила из §7 (новый получатель, 150 THB):
{
"required": "PIN",
"reasonCode": "p2p_new_payee_over_100thb",
"policyId": 12,
"context": {
"dailyCumulative": "0",
"isNewPayee": true
}
}
Если required пришёл NONE с reasonCode: "no_match" — правило не совпало. Возможные причины:
merchantIdне передан, а в условииnew_payee: true.- Сумма указана в батах, а не в сатангах (
"100"вместо"10000"). channelилиcurrencyне совпадают с условием (точное сравнение, без приведения регистра).- В правиле использован
app:wallet, а в запросеappId: "miniapp-x". - Сработало другое правило с меньшим
priority— посмотритеpolicyIdв ответе.
10. Шаг 8 — тесты¶
Добавьте кейс в test/limits/evaluate-policy.test.ts. Шаблон (mock DB через chainable thenable, см. makeDbChain в существующих тестах):
it('returns PIN for INTERNAL_P2P to new payee with amount >= 100 THB', async () => {
dbMock.select
.mockReturnValueOnce(makeDbChain([{ sumUsed: '0' }])) // computeAuthContext: daily
.mockReturnValueOnce(makeDbChain([{ cnt: '0' }])) // computeAuthContext: isNewPayee
.mockReturnValueOnce(makeDbChain([{
id: 12, scope: 'global',
condition: { channel: 'INTERNAL_P2P', new_payee: true, amount_gte: 10_000 },
requiredStepUp: 'PIN', reasonCode: 'p2p_new_payee_over_100thb',
priority: 12, active: true,
}]))
const result = await evaluatePolicy({
userId: 1, merchantId: 42, amount: 15_000n,
currency: 'THB', channel: 'INTERNAL_P2P',
appId: 'wallet', operationType: 'P2P_TRANSFER',
})
expect(result.required).toBe('PIN')
expect(result.reasonCode).toBe('p2p_new_payee_over_100thb')
expect(result.context.isNewPayee).toBe(true)
})
Запуск:
Для интеграционного теста endpoint (с HMAC-обвязкой) — см. test/policies/routes.test.ts.
11. Шаг 9 — CHANGELOG.md¶
Кратко зафиксируйте новое правило в projects/payment-manager/CHANGELOG.md — это аудиторский след для compliance:
## [Unreleased]
### Added
- auth-policy: PIN при P2P new payee с суммой ≥ 100 THB (priority 12, reason `p2p_new_payee_over_100thb`).
12. Когда нужен новый ключ condition¶
Если ни один из существующих ключей не покрывает кейс (например, нужен time_of_day или device_risk_score):
- В
src/limits/evaluate-policy.tsрасширитьmatchesCondition()— добавить веткуif (cond.<key> != null) .... - Если ключу нужны новые поля контекста — расширить
AuthContextиcomputeAuthContext(). - Если ключу нужны новые поля запроса — расширить
PolicyRequestиEvalReqSchemaвsrc/policies/routes.ts. - Покрыть тестами в
test/limits/evaluate-policy.test.ts. - Обновить ../../AUTH-POLICIES.md — таблицу поддерживаемых ключей.
- После этого можно вставлять политики с новым ключом.
13. Связанные документы¶
- ../../AUTH-POLICIES.md — справочник полей и SQL-примеры.
- ../api/auth.md — схема HMAC-подписи.
src/limits/evaluate-policy.ts— matcher.src/policies/routes.ts— endpointPOST /policies/evaluate.drizzle/migrations/0008_auth_policies.sql— DDL + seed.test/limits/evaluate-policy.test.ts— unit-тесты matcher'а.test/policies/routes.test.ts— интеграционные тесты endpoint'а.