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

Интеграция с 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-парсера.

Типичный поток платежа

  1. Quote (опционально). UI спрашивает POST /intents/quote — PM считает fee/limits, возвращает разбивку для подтверждающего экрана. Запись в БД не создаётся.
  2. Create. Auth Center вызывает POST /intents с fromAccountName, toAccountName, amount, operationType, idempotencyKey. PM возвращает интент в статусе PENDING_USER_CONFIRMATION или сразу AUTH_REQUIRED (если сработала step-up политика).
  3. Policy evaluate (опционально). Если UI хочет заранее знать о step-up — POST /policies/evaluate до создания интента.
  4. Confirm. После step-up auth (PIN/biometric во Flutter, токен в Auth Center) — POST /intents/:id/confirm. PM запускает сагу: lock → fees → settle → publish.
  5. Stream. Flutter висит на streamStatus(intentId) — Auth Center проксирует Redis pub/sub в Serverpod-стрим.
  6. 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.
  • На streamStatus disconnect — переоткрывать стрим и делать 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 — кросс-сервисные правила и архитектурная диаграмма.