API: Step-Up Policies¶
Endpoints для оценки требуемого уровня дополнительной аутентификации (StepUp) перед платёжной операцией. Вычисление только read-only — движок не пишет в БД и не изменяет состояние.
Связанные документы:
- Модуль и внутренняя логика matcher-а:
../modules/policies.md. - Схема таблицы правил:
../reference/database/08-auth-policies.md.
Общее¶
Все policy-эндпоинты находятся под HMAC-защищённым scope (см. ./auth.md). Запросы делает Auth Center (Serverpod) перед созданием инвойса или перевода; реже — mini-app бэкенды для предсказания UX. Admin Panel /policies/* не вызывает (административные операции выполняются service-to-service без StepUp).
Обязательные заголовки¶
| Header | Назначение |
|---|---|
X-Service-Id |
Идентификатор сервиса-инициатора (auth-center, mini-app-x). |
X-Timestamp |
Unix-секунды на момент подписи (окно ±60 с). |
X-Signature |
HMAC-SHA256 от канонической строки TS\nMETHOD\nPATH\nSHA256(body). |
X-User-Id |
ID пользователя-инициатора (контекст user-операции). |
Опциональные заголовки¶
| Header | Назначение |
|---|---|
X-App-Id |
Идентификатор приложения-источника (wallet, mini-app-x). Сейчас передаётся в body (appId); header — план на будущее (см. spec MERCHANT_INVOICE). |
Уровни StepUp (StepUpLevel)¶
Возвращаемое значение required — одно из пяти:
| Уровень | Что требуется от пользователя |
|---|---|
NONE |
Подтверждение не нужно — операция проходит сразу. |
PIN |
Ввод 4–6-значного PIN-кода. |
BIOMETRIC |
Touch ID / Face ID (платформенная биометрия). |
OTP |
Одноразовый код по SMS / push / e-mail. |
KYC_UPLIFT |
Поднять уровень KYC (загрузить документы, селфи и т. д.). |
Определение типа: src/shared/schema.ts:330.
1. POST /policies/evaluate¶
Назначение¶
Вычисляет требуемый StepUpLevel для конкретной платёжной операции на основе активных правил из pm.auth_policies. Возвращает первое совпавшее правило (с наименьшим priority) или NONE, если ни одно условие не выполнилось.
Источник: src/policies/routes.ts:50.
Authentication¶
- HMAC (см.
./auth.md) — обязательно. X-User-Id— обязательно (операция выполняется в контексте конкретного пользователя; используется для вычисленияdailyCumulativeиisNewPayee).
Request body¶
Content-Type: application/json.
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
userId |
integer (> 0) |
да | ID пользователя-инициатора. Должен совпадать с X-User-Id. |
amount |
string (^\d+$) |
да | Сумма в минимальных единицах валюты (satang для THB). Строка — чтобы избежать потери точности. |
currency |
string (3 симв.) | да | ISO 4217: THB, USD, ... |
channel |
string (1–50) | да | Канал платежа: INTERNAL_P2P, IPPS_TRANSFER, MERCHANT_INVOICE, ADMIN и т. д. (см. ../reference/passport/02-channels.md). |
appId |
string (1–50) | да | Идентификатор приложения-источника. Используется для scope app:<appId> при выборке политик. |
operationType |
string (1–50) | да | Тип операции: INVOICE_PAYMENT, P2P_TRANSFER, и т. п. (см. ../reference/passport/03-operation-types.md). |
merchantId |
integer (> 0) |
нет | ID мерчанта-получателя. Если указан — добавляется scope merchant:<id> и вычисляется isNewPayee. |
Замечание. Источник scope — комбинация полей
appId+merchantId. Самостоятельного поляscopeв теле запроса нет; движок сам строит список областей[global, app:<appId>, merchant:<merchantId>?]и выбирает активные правила из этих scope.
Response 200 — PolicyDecision¶
{
"required": "PIN", // StepUpLevel: NONE | PIN | BIOMETRIC | OTP | KYC_UPLIFT
"reasonCode": "large_amount", // машинно-читаемый код причины (для UI / аудита)
"policyId": 42, // ID правила из pm.auth_policies (0 — нет совпадения, дефолт NONE)
"context": {
"dailyCumulative": "150000", // дневной оборот пользователя (минимальные единицы; строка)
"isNewPayee": true // true — пользователь раньше не платил этому мерчанту
}
}
| Поле | Тип | Описание |
|---|---|---|
required |
StepUpLevel |
Требуемый уровень аутентификации. Auth Center сам решает, как собрать фактор (PIN-pad, FaceID, OTP, KYC challenge). |
reasonCode |
string | Код причины из правила (reason_code в pm.auth_policies). Для дефолтного NONE-ответа — no_match. |
policyId |
number | ID совпавшего правила. 0 — ни одно правило не сработало (вернулся NONE). |
context.dailyCumulative |
string (BigInt) | Сумма успешных дебет-транзакций пользователя в данной валюте с начала суток UTC. Сериализована строкой. |
context.isNewPayee |
boolean | true, если у пользователя нет ни одного SETTLED-перевода данному merchantId (актуально только если merchantId передан). |
Алгоритм оценки (кратко)¶
- Считается
AuthContext:dailyCumulative(суммa SETTLED-дебетов за сегодня) иisNewPayee(если естьmerchantId). - Загружаются активные правила (
active = true) для scope-овglobal,app:<appId>и, если задан,merchant:<merchantId>, отсортированные поpriority ASC. - Для каждого правила проверяется
condition(AND по ключам:currency,channel,amount_lt,amount_gte,amount_lte,daily_cumulative_gte,new_payee). Первое совпавшее — финальное. - Если ни одно не сработало — возвращается
{ required: 'NONE', reasonCode: 'no_match', policyId: 0 }.
Подробнее о matcher-е: ../modules/policies.md, исходник: src/limits/evaluate-policy.ts.
Errors¶
| HTTP | Условие | Тело |
|---|---|---|
| 400 | Невалидное тело (Zod-валидация: формат amount, длина currency). |
{ statusCode, error: 'Bad Request', message } |
| 401 | Отсутствует / просрочена HMAC-подпись. | { error: 'unauthorized', reason } (см. ./auth.md) |
| 403 | HMAC-сервис не имеет прав вызывать /policies/*. |
{ error: 'forbidden' } |
| 500 | Ошибка чтения БД или непредвиденное исключение. | { statusCode: 500, error: 'Internal Server Error' } |
Эндпоинт не возвращает 404 для «нет правила» — это нормальный кейс, отдаётся 200 { required: 'NONE' }.
Curl-пример¶
# Подготовка
BODY='{"userId":1,"amount":"200000","currency":"THB","channel":"IPPS_TRANSFER","appId":"wallet","operationType":"P2P_TRANSFER","merchantId":42}'
TS=$(date +%s)
HASH=$(printf '%s' "$BODY" | sha256sum | awk '{print $1}')
MSG=$(printf '%s\nPOST\n/policies/evaluate\n%s' "$TS" "$HASH")
SIG=$(printf '%s' "$MSG" | openssl dgst -sha256 -hmac "$AUTH_CENTER_SECRET" -hex | awk '{print $2}')
curl -X POST http://pm.internal/policies/evaluate \
-H "Content-Type: application/json" \
-H "X-Service-Id: auth-center" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG" \
-H "X-User-Id: 1" \
-d "$BODY"
Пример успешного ответа (правило large_amount сработало для суммы 2000.00 THB):
{
"required": "PIN",
"reasonCode": "large_amount",
"policyId": 5,
"context": { "dailyCumulative": "0", "isNewPayee": false }
}
Идемпотентность и побочные эффекты¶
- Эндпоинт идемпотентный — повторный вызов с теми же параметрами вернёт тот же результат (если правила и история транзакций не изменились).
- Не пишет в БД, не меняет состояние интентов, не создаёт Outbox-событий.
- Не кэшируется на стороне PM — каждый вызов перечитывает
pm.auth_policiesиpm.tx_history. Изменения правил применяются мгновенно.
Поддерживаемые ключи condition¶
Все ключи внутри condition (JSONB-поле правила) применяются по AND. Если ключ не указан — он не влияет на проверку.
| Ключ | Тип | Семантика |
|---|---|---|
currency |
string | Совпадает с body.currency. Например, "THB". |
channel |
string | Совпадает с body.channel. Например, "IPPS_TRANSFER". |
amount_lt |
number | body.amount < value (строго меньше). |
amount_gte |
number | body.amount >= value. |
amount_lte |
number | body.amount <= value. |
daily_cumulative_gte |
number | context.dailyCumulative >= value. |
new_payee |
boolean | context.isNewPayee == value. Имеет смысл только при наличии merchantId. |
Числовые значения сравниваются как BigInt — точность не теряется. Поле operationType пока не участвует в condition напрямую (фильтрация по типу делается через scope и channel).
Примеры разных уровней StepUp¶
NONE (правило не сработало)¶
Запрос: малая сумма, дневной оборот 0, не новый получатель.
{ "required": "NONE", "reasonCode": "no_match", "policyId": 0,
"context": { "dailyCumulative": "0", "isNewPayee": false } }
PIN (большая сумма)¶
Правило: condition = { "amount_gte": 100000 }, required_step_up = "PIN", reason_code = "large_amount".
{ "required": "PIN", "reasonCode": "large_amount", "policyId": 5,
"context": { "dailyCumulative": "0", "isNewPayee": false } }
BIOMETRIC (новый получатель)¶
Правило: scope = "merchant:*", condition = { "new_payee": true }, required_step_up = "BIOMETRIC".
{ "required": "BIOMETRIC", "reasonCode": "new_payee", "policyId": 9,
"context": { "dailyCumulative": "100000", "isNewPayee": true } }
OTP (порог дневного оборота)¶
Правило: condition = { "daily_cumulative_gte": 500000 }, required_step_up = "OTP".
{ "required": "OTP", "reasonCode": "high_daily_volume", "policyId": 12,
"context": { "dailyCumulative": "520000", "isNewPayee": false } }
KYC_UPLIFT (порог KYC tier)¶
Правило: condition = { "amount_gte": 5000000 }, required_step_up = "KYC_UPLIFT".
{ "required": "KYC_UPLIFT", "reasonCode": "kyc_tier_required", "policyId": 20,
"context": { "dailyCumulative": "0", "isNewPayee": false } }
Идемпотентность и побочные эффекты¶
- Эндпоинт идемпотентный — повторный вызов с теми же параметрами вернёт тот же результат (если правила и история транзакций не изменились).
- Не пишет в БД, не меняет состояние интентов, не создаёт Outbox-событий.
- Не кэшируется на стороне PM — каждый вызов перечитывает
pm.auth_policiesиpm.tx_history. Изменения правил применяются мгновенно. - Производительность: 2 коротких SELECT по индексу + 1 SELECT по
auth_policies_lookup_idx. p95 < 20 ms в стабильной нагрузке.
Best practices для вызывающих¶
- Всегда передавайте
merchantIdдля платежей мерчанту — без него политикаnew_payeeне сработает, и пользователь может оплатить нового получателя без BIOMETRIC. - Не кэшируйте
PolicyDecisionна стороне Auth Center дольше, чем длится одна сессия StepUp —dailyCumulativeменяется после каждого SETTLED-перевода. - Обрабатывайте
NONEявно — это валидный ответ, не ошибка. Auth Center должен просто пропустить шаг StepUp. - Логируйте
policyIdиreasonCodeв трассировке — это ключ для аудита («почему пользователь увидел OTP-челлендж?»). - Используйте одинаковый
appIdдля всех вызовов из одного клиента (например,wallet), иначе scopeapp:<appId>не сможет таргетировать правила.
Связанные эндпоинты¶
POST /intents— после успешного StepUp Auth Center создаёт интент.- Управление правилами
pm.auth_policies— пока только через прямой SQL / Admin Panel (отдельных REST-эндпоинтов нет).
Ссылки¶
- Модуль и matcher:
../modules/policies.md. - Таблица правил:
../reference/database/08-auth-policies.md. - Каналы:
../reference/passport/02-channels.md. Типы операций:../reference/passport/03-operation-types.md. Auth:./auth.md. - Исходники:
src/policies/routes.ts,src/limits/evaluate-policy.ts,src/shared/schema.ts:330.