# Phase 8 Backend Contract — Customer Identity, VK ID, MAX/Telegram Messaging Companion to [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md) Phase 8 (Sprints 8.1–8.5). Covers plan §2.9, §3.4, and all of §14 (the v3.1-only addition). **Status: ready to build. Sprint order fixed by Sprint 0.1 decision: VK ID first, then everything else** ("do all after vk"). Sequence below follows that: identity core → VK ID → email/phone OTP → MAX/Telegram → Notification Orchestrator. --- ## 1. Entities ```ts interface Customer { id: string; marketplaceId: string; // or global identity strategy, tenant-configurable name?: string; email?: string; phone?: string; status: 'active' | 'suspended'; createdAt: string; } interface ExternalIdentity { customerId: string; provider: 'vk_id' | 'yandex_id' | 'telegram' | 'max'; // yandex_id added 2026-08-21, FH-4.5 providerUserId: string; email?: string; // VK frequently returns none - never require it phone?: string; displayName?: string; verifiedAt: string; metadata: Record; lastUsedAt: string; } interface ContactMethod { customerId: string; type: 'email' | 'phone'; value: string; verifiedAt?: string; } interface ContactChannel { customerId: string; provider: 'telegram' | 'vk' | 'max'; chatId: string; verified: boolean; notificationsEnabled: boolean; deliveryEnabled: boolean; } interface MessagingConsent { customerId: string; channel: string; purpose: 'marketing' | 'order_service_messages'; grantedAt?: string; revokedAt?: string; } ``` Telegram is demoted from sole identity to one `ExternalIdentity` provider among several — it must remain fully functional, just no longer the only path. ## 2. Sprint 8.2 — Social identity: VK ID first, Yandex ID second Rewritten 2026-08-21 (FH-4.1–FH-4.5). Previously this section specified a VK-only pair of endpoints where the callback took `{ code, codeVerifier }` from the browser. Two changes: the surface is provider-agnostic, and the PKCE verifier stops travelling through the client. ### 2.1 Surface ``` GET /api/identity/v1/{provider}/authorize?returnTo= -> { url } (or 302) GET /api/identity/v1/{provider}/callback?code=&state=[&device_id=] POST /api/identity/v1/{provider}/unlink (authenticated) GET /api/identity/v1/me/identities (authenticated) -> ExternalIdentity[] ``` `{provider}` is `vk` or `yandex` today; `telegram` and `max` join it when §4 migrates them onto `ExternalIdentity`. One controller, one strategy object per provider. The frontend gateway is a single interface (`src/app/core/identity/services/social-identity-gateway.interface.ts`) — providers differ only in a path segment, because everything that actually differs between them is backend-side. ### 2.2 The backend owns state and the code verifier `/authorize` generates `state` and `code_verifier`, stores `{ state, codeVerifier, marketplaceId, returnTo, expiresAt }` server-side or in a signed `HttpOnly` cookie, TTL 10 minutes, **single use — deleted on first presentation**. It returns (or redirects to) the provider URL carrying `code_challenge` (S256) and `state`. `/callback` validates `state`, exchanges the code using the stored verifier, fetches the profile, resolves or links the `ExternalIdentity`, issues the customer session cookie ([Track S §2.1](TRACK-S-SECURITY-RBAC-CONTRACT.md)), and redirects to `returnTo`. - The client never sees a client secret, an access token, or a code verifier. We are a confidential client; a browser-held verifier buys nothing and adds a place to steal it from. - An unknown, expired, or replayed `state` is a generic error. Do not distinguish the cases to the caller. - `returnTo` is validated against the tenant's own verified origin. It is an open redirect otherwise. ### 2.3 Identity resolution - A repeat login for the same `providerUserId` resolves to the same `Customer`, never a duplicate. - `UNIQUE (provider, providerUserId)`. If a provider account is already bound to a *different* `Customer`, that is an identity conflict — route to controlled resolution, never silently rebind (plan §14.3). The unique index is what enforces this; a service-layer check is not sufficient. - Whether one VK account across two of our storefronts is one `Customer` or two is a **product decision that must be made before implementation** (FH-0.1). `Customer.marketplaceId` currently implies two, and two is the safer default for data protection. - Email is optional on `Customer`. Yandex returns one in most cases; VK frequently does not. ### 2.4 Per-tenant OAuth applications Client id and secret are per marketplace, stored with the [Track S §4.2](TRACK-S-SECURITY-RBAC-CONTRACT.md) envelope: `{ clientId, clientSecret, scopes[], redirectUri }`. Never returned by any endpoint. **The redirect_uri problem, which must be solved before any code is written.** Both providers validate `redirect_uri` against an exact registered list. We cannot register one per tenant domain and we cannot let tenants supply their own. Resolution: one **central identity host** as the sole registered callback, the origin tenant carried inside the signed `state`, then a 302 back to the tenant domain with a short-lived signed one-time handoff token that the tenant API exchanges for its session cookie. This is a one-way door — retrofitting it after the first provider is live is expensive. ### 2.5 Provider notes Confirm exact parameter and scope names against live provider documentation before implementing; both providers have revised their flows recently. **VK ID** — OAuth 2.1, PKCE mandatory (S256). ``` authorize GET https://id.vk.com/authorize client_id, redirect_uri, response_type=code, code_challenge, code_challenge_method=S256, state, scope token POST https://id.vk.com/oauth2/auth grant_type=authorization_code, code, code_verifier, device_id, client_id, redirect_uri profile POST https://id.vk.com/oauth2/user_info logout https://id.vk.com/oauth2/logout (call on unlink) ``` **The callback returns `device_id` alongside `code`, and the token exchange fails without it.** This is the single most common VK ID integration bug; it is in this contract so it is not rediscovered at debugging time. **Yandex ID** — OAuth 2.0, PKCE supported; use it. ``` authorize GET https://oauth.yandex.ru/authorize response_type=code, client_id, redirect_uri, state, code_challenge, code_challenge_method=S256 token POST https://oauth.yandex.ru/token grant_type=authorization_code, code, code_verifier HTTP Basic: client_id:client_secret profile GET https://login.yandex.ru/info?format=json Authorization: OAuth -> id, login, default_email, default_phone, psuid ``` Yandex is a second strategy object against the same surface, not a second integration. Build it after VK works. ## 3. Sprint 8.3 — Email/phone OTP (after VK ID) Implements the already-approved [email/phone login spec](../superpowers/specs/2026-08-15-email-phone-login-design.md). Per v3.1 §14, position this as **recovery/fallback** when a messenger channel is unavailable — not the primary login path. No new contract beyond that spec; this section exists only to fix its place in the build order relative to VK ID. ## 4. Sprint 8.4 — MAX + Telegram bot channels ```ts interface BotConversationBinding { customerId: string; marketplaceId: string; provider: 'telegram' | 'max'; chatId: string; state: string; // see §5 state machine orderId?: string; lastMessageAt: string; } ``` MAX linking flow (bot-assisted, one-time code): ``` POST /api/identity/v1/max/link-code -> { code, expiresAt } (TTL, single-use, bound to marketplace + browser session) ``` User opens the MAX bot, sends the code; a confirmed bot update on the backend calls: ``` POST /api/providers/v1/max/bot-webhook -- idempotent; a repeated update must not create a duplicate binding ``` which links the pending `Customer` session to the MAX `chatId`. All three providers' incoming bot updates (VK, MAX, Telegram) normalize into one shape: ```ts interface MessagingEvent { provider: 'telegram' | 'vk' | 'max'; chatId: string; orderId?: string; text?: string; receivedAt: string; } ``` Provider bot tokens/secrets never reach the frontend, ever — only the backend calls each provider's Bot API. ## 5. Sprint 8.5 — Notification Orchestrator + Delivery Conversation State Machine On `order.paid` (Phase 2 event bus), the orchestrator picks the customer's chosen channel (captured at checkout, see [Phase 6](PHASE-6-CART-CHECKOUT-CONTRACT.md) and `OrderContactSnapshot` in [Phase 2](PHASE-2-ORDERS-NOTIFICATIONS-CONTRACT.md)) and drives: ``` not_started -> awaiting_customer -> details_received -> manager_assigned/auto_confirmed -> shipment_planned -> completed ``` ```ts interface DeliveryDetailsSnapshot { orderId: string; city?: string; address?: string; recipientName?: string; phone?: string; timeWindow?: string; comment?: string; receivedAt: string; } ``` Hard rules: - **The bot never changes financial statuses.** It can only write `DeliveryDetailsSnapshot` fields via a dedicated Delivery Service — no bot code path touches `Order.paymentStatus`/`orderStatus`. - The backoffice `Notification` (Phase 2 §6) fires unconditionally on `order.paid`, independent of whether the customer's messenger channel is reachable. - If the chosen channel is unavailable, log a `DeliveryAttempt` error (Phase 2 §6) and fall back per tenant-configured policy (e.g. email/SMS) — never block the order itself. - Follow-up messages are rate-limited per tenant policy; after the configured attempt limit, hand off to a human manager instead of continuing to message. ``` POST /api/providers/v1/{provider}/bot-webhook -- generic entrypoint for all three providers GET /api/admin/v2/orders/{orderId}/conversation -- message history + current state, for manager handoff POST /api/admin/v2/orders/{orderId}/conversation/handoff ``` ## 6. What the frontend will start doing once this ships - VK ID and Yandex ID login buttons on the storefront. The client half already exists and is provider-agnostic: `SocialLoginButtonComponent` behind `SOCIAL_IDENTITY_GATEWAY`, with a real HTTP gateway waiting on §2.1's endpoints. Adding Yandex once VK works is a `provider` input, not new code. - Account linking screen (`GET /me/identities`, link/unlink), including the identity-conflict resolution path from §2.3. - MAX/Telegram linking UI (one-time code flow). - Checkout channel-choice step ("where should we send confirmation?") — VK / MAX / Telegram / email/SMS fallback. - Manager-facing conversation view (message history, current delivery state, accept handoff).