Интеграция с Auth Center¶
Auth Center (Serverpod/Dart) — главный потребитель Payment Manager: создаёт платёжные интенты по HMAC, читает баланс пользователя через nginx-прокси и подписывается на realtime-статусы интента через Redis pub/sub. PM не имеет обратной зависимости — он не знает о схеме public.* и не вызывает Serverpod-эндпоинты.
Назначение¶
Auth Center — это бэкенд OneWallet (Serverpod, Dart), который держит JWT, профиль пользователя, KYC и каталог мини-приложений. На уровне платежей он выполняет роль клиента PM:
- формирует интенты от имени пользователя (
POST /intentsи связанные); - проверяет step-up auth-политики (
POST /policies/evaluate); - читает баланс пользователя
GET /api/pm/accounts/balanceчерез nginx (проксиauth_request → PM); - стримит live-статус интента Flutter-приложению (
streamStatus()), подписываясь на Redis-канал PM-а.
Все вызовы Auth Center → PM подписываются HMAC по контракту auth.md: заголовки X-Service-Id, X-Timestamp, X-Signature. Для Auth Center зарезервированы два service-key (см. ниже) — никаких «доверенных» вызовов без подписи быть не должно (см. NO-GO).
Endpoints, которые зовёт Auth Center¶
Все эндпоинты вызываются через HTTP с HMAC-подписью. Полные схемы — в ../api/.
| Метод и путь | Назначение | Документация |
|---|---|---|
POST /intents |
Создать интент платежа (P2P, withdrawal, mini-app charge/credit, invoice payment и т.д.). | intents.md |
POST /intents/quote |
Получить расчёт fee/limits без записи в БД — для предварительного отображения суммы в UI. | intents.md |
POST /intents/:id/confirm |
Подтвердить интент после step-up auth (PIN, biometrics). | intents.md |
POST /intents/:id/cancel |
Отменить интент в статусе PENDING_USER_CONFIRMATION / AUTH_REQUIRED. |
intents.md |
POST /policies/evaluate |
Проверить, нужен ли step-up для конкретной операции (сумма, тип, dynamic conditions). | policies.md |
GET /intents/:id |
Получить актуальное состояние интента (статус, fees, channel). | intents.md |
GET /intents/:id/events |
История событий по интенту (audit, статусные переходы, причины). | intents.md |
Все ответы PM возвращают intentId и набор полей, согласованных с парсером IntentDto во Flutter — см. PASSPORT.md (раздел контракта). Любое изменение DTO требует синхронного обновления Auth Center и Flutter-парсера.
Типичный поток платежа¶
- Quote (опционально). UI спрашивает
POST /intents/quote— PM считает fee/limits, возвращает разбивку для подтверждающего экрана. Запись в БД не создаётся. - Create. Auth Center вызывает
POST /intentsсfromAccountName,toAccountName,amount,operationType,idempotencyKey. PM возвращает интент в статусеPENDING_USER_CONFIRMATIONили сразуAUTH_REQUIRED(если сработала step-up политика). - Policy evaluate (опционально). Если UI хочет заранее знать о step-up —
POST /policies/evaluateдо создания интента. - Confirm. После step-up auth (PIN/biometric во Flutter, токен в Auth Center) —
POST /intents/:id/confirm. PM запускает сагу: lock → fees → settle → publish. - Stream. Flutter висит на
streamStatus(intentId)— Auth Center проксирует Redis pub/sub в Serverpod-стрим. - Reconcile. При reconnect / mobile-suspend — Auth Center делает
GET /intents/:idдля catch-up; Redis pub/sub не гарантирует доставку при разрыве.
Service keys¶
Auth Center использует два HMAC service-key. Оба засеяны в drizzle/seed.ts и проверяются src/auth/hmacPlugin.ts через SERVICE_SECRETS.
auth-center (основной)¶
Покрывает большинство потоков от имени пользователя:
allowedOperationTypes:P2P_TRANSFER,IPPS_WITHDRAWAL,THAI_QR_PAY,WITHDRAWAL,QP_TOPUP,MINIAPP_CHARGE,MINIAPP_CREDIT,INVOICE_PAYMENT.fromAccountOverride/toAccountOverride: разрешены типыUSER_WALLET,MERCHANT_WALLET,AGENT_WALLET;namePattern = ^(user\.|merchant\.|agent\.).allowToTbAccountId: true— допускается прямая адресация по TigerBeetle account ID (для P2P по реквизитам).
auth-center-merchant (NFC pull-charge)¶
Узко ограниченный key для NFC-touch-to-pay сценария: терминал мерчанта инициирует charge с пользовательского/агентского кошелька.
allowedOperationTypes: толькоNFC_CHARGE. Никогда не выдавать ему ничего другого.fromAccountOverride: типыUSER_WALLET,AGENT_WALLET;namePattern = ^(user\.|agent\.).toAccountOverride: типMERCHANT_WALLET;namePattern = ^merchant\..allowToTbAccountId: false— никаких raw TB ID; только именованные счета.
Маски enforced'ятся на уровне PM (enforceServicePermissions) поверх валидации DTO — primary defence по allowedTypes, secondary по namePattern.
Live-обновления статуса интента: streamStatus()¶
После создания интента Flutter-клиент через Auth Center открывает Serverpod-стрим paymentEndpoint.streamStatus(intentId). Реализация Auth Center внутри подписывается на Redis-канал intent.<intentId> PM-а и пробрасывает каждое сообщение в Serverpod-стрим как IntentStatusUpdate.
PM публикует обновления через publishIntentStatus() (src/intent/intent-events.ts):
// src/intent/intent-events.ts
await getRedis().publish(`intent.${intentId}`,
JSON.stringify({ intentId, status, updatedAt: new Date().toISOString() }))
Payload — JSON: { intentId, status, updatedAt }. Publish — best-effort: ошибка Redis не валит транзакцию интента (writeIntentEvent пишет intent_event в БД даже при сбое publish'а — это источник истины). Auth Center должен делать resync через GET /intents/:id при reconnect/disconnect стрима — Redis pub/sub не гарантирует доставку.
Список статусов и их семантика — intents.md.
Account name resolution — явный контракт¶
Memory-инвариант (
feedback-account-resolution): Auth Center всегда передаёт явныеfromAccountNameиtoAccountNameв телеPOST /intents. PM никогда не делает auto-resolve поuserId.
Это сознательное проектное решение:
- Auth Center знает контекст операции (выбор счёта в UI, источник миниапа, NFC-терминал) лучше, чем PM. Имя счёта — часть бизнес-намерения, не вычисляемое значение.
- Auto-resolve по
userIdсоздавал бы скрытую зависимость PM от схемыpublic.*Auth Center'а (правила маппинга «один пользователь → какой счёт по умолчанию»). PM эту схему не читает. - Явные имена делают аудит и трассировку (
trace_id = intent_id) однозначными: в каждом intent-row уже зафиксированыfromAccountName/toAccountName, без необходимости восстанавливать «что бы PM выбрал в тот момент».
Связанный инвариант (feedback-limit-direction): направление лимита (DEBIT / CREDIT) определяется PM как from.tb_account_map.userId === X-User-Id ? DEBIT : CREDIT. Поэтому Auth Center обязан класть в заголовок X-User-Id customer's userId (того, кто платит) — не мерчанта. Иначе NFC-лимиты nfc_per_tap / nfc_daily молча обходятся (limitDirection = CREDIT не матчится правилам с direction = DEBIT).
Чтение баланса — через nginx, не напрямую¶
Auth Center не имеет клиента TigerBeetle и не запрашивает PM напрямую для отображения баланса в UI. Поток:
Flutter App ──► nginx (/api/pm/accounts/balance)
│
├── auth_request → Auth Center (JWT-валидация, X-User-Id injection)
└── proxy_pass → PM (HMAC от service-id=nginx-gateway)
nginx-gateway — отдельный service-key с пустым allowedOperationTypes (read-only). Auth Center в этом потоке выступает как auth-валидатор, а не как платёжный клиент.
Для серверных задач (например, посчитать баланс кошелька внутри Serverpod-эндпоинта без участия Flutter) Auth Center может вызвать тот же эндпоинт прямым HTTP, но это не предпочтительный путь — балансовые отображения должны идти через клиента.
NO-GO¶
| # | Запрет | Почему |
|---|---|---|
| 1 | Auth Center не пишет в схему pm.*. |
PM — единственный writer для платёжной БД; пересечения схем запрещены root CLAUDE.md. |
| 2 | Auth Center не имеет TigerBeetle SDK / клиента. | TB — закрытый ресурс PM (см. CLAUDE.md «Доступ к TigerBeetle»). Любой расчёт баланса — через PM. |
| 3 | Auth Center может читать собственную схему public.*, не может читать pm.*. |
Изоляция миграций (serverpod generate vs drizzle-kit migrate) и предотвращение неявных связей. |
| 4 | Auth Center не вызывает PM без HMAC. | Никаких «доверенных» вызовов — hmacPlugin отклоняет любой запрос без валидной подписи. |
| 5 | auth-center-merchant не выдаётся на операции, отличные от NFC_CHARGE. |
Узкий scope ключа — единственная защита от подмены направления pull-charge'а. |
| 6 | Auth Center не выдумывает fromAccountName / toAccountName за пользователя без его явного выбора в UI. |
Имена счетов — часть аудируемого бизнес-намерения; PM не делает auto-resolve. |
Идемпотентность и retry¶
Все мутирующие вызовы Auth Center → PM обязаны передавать idempotencyKey в теле запроса. PM хранит (serviceId, idempotencyKey) → intentId в pm.intent (unique index) и возвращает существующий интент при повторе с тем же ключом.
Рекомендация по генерации ключа:
- Для пользовательских действий — UUIDv4, сгенерированный на стороне Flutter и пробрасываемый Auth Center'ом без изменений (чтобы retry с того же экрана не создавал второй интент).
- Для серверных задач (webhooks из мини-аппов через Auth Center) — детерминированный hash
${sourceEvent.id}:${operationType}.
Retry-политика Auth Center:
- На сетевые ошибки и 5xx — exponential backoff, не более 3 попыток, тот же
idempotencyKey. - На 4xx (включая
409 IDEMPOTENCY_CONFLICT,422 VALIDATION_FAILED) — не ретраить, пробрасывать ошибку в UI. - На
streamStatusdisconnect — переоткрывать стрим и делатьGET /intents/:idдля resync, не пересоздавать интент.
Коды ошибок PM — см. api/errors.md.
Связанные документы¶
- api/intents.md — схемы и статусы интентов.
- api/policies.md — step-up auth и
policies/evaluate. - api/auth.md — HMAC-контракт, обязательные заголовки.
- PASSPORT.md — канонический контракт PM, синхронизация DTO.
- Корневой CLAUDE.md — кросс-сервисные правила и архитектурная диаграмма.